Merge pull request #38 from YouJustDontKnow/v5-udp-transport
v5 UDP packet handling
diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/AuthenticationProvider.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/AuthenticationProvider.kt
new file mode 100644
index 0000000..562c7e8
--- /dev/null
+++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/AuthenticationProvider.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.v5
+
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.devp2p.v5.misc.AuthHeader
+import org.apache.tuweni.devp2p.v5.misc.HandshakeInitParameters
+import org.apache.tuweni.devp2p.v5.misc.SessionKey
+
+/**
+ * Module for securing messages communications. It creates required parameters for peers handshake execution.
+ * All session keys information is located here, which are used for message encryption/decryption
+ */
+interface AuthenticationProvider {
+
+ /**
+ * Creates authentication header to initialize handshake process. As a result it creates an authentication
+ * header to include to udp message.
+ *
+ * @param handshakeParams parameters for authentication header creation
+ *
+ * @return authentication header for handshake initialization
+ */
+ fun authenticate(handshakeParams: HandshakeInitParameters): AuthHeader
+
+ /**
+ * Verifies, that incoming authentication header is valid via decoding authorization response and checking
+ * nonce signature. In case if everything is valid, it creates and stores session key
+ *
+ * @param senderNodeId sender node identifier
+ * @param authHeader authentication header for verification
+ */
+ fun finalizeHandshake(senderNodeId: Bytes, authHeader: AuthHeader)
+
+ /**
+ * Provides session key by node identifier
+ *
+ * @param nodeId node identifier
+ *
+ * @return session key for message encryption/decryption
+ */
+ fun findSessionKey(nodeId: String): SessionKey?
+
+ /**
+ * Persists session key by node identifier
+ *
+ * @param nodeId node identifier
+ * @param sessionKey session key
+ */
+ fun setSessionKey(nodeId: String, sessionKey: SessionKey)
+}
diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/MessageHandler.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/MessageHandler.kt
new file mode 100644
index 0000000..8448492
--- /dev/null
+++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/MessageHandler.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.v5
+
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.devp2p.v5.packet.UdpMessage
+import java.net.InetSocketAddress
+
+/**
+ * Udp message handler, aimed to process it's parameters and sending result
+ */
+interface MessageHandler<T : UdpMessage> {
+
+ /**
+ * @param message udp message containing parameters
+ * @param address sender address
+ * @param srcNodeId sender node identifier
+ * @param connector connector for response send if required
+ */
+ fun handle(message: T, address: InetSocketAddress, srcNodeId: Bytes, connector: UdpConnector)
+}
diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/NodeDiscoveryService.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/NodeDiscoveryService.kt
new file mode 100644
index 0000000..9a0fcc0
--- /dev/null
+++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/NodeDiscoveryService.kt
@@ -0,0 +1,99 @@
+/*
+ * 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.v5
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.crypto.SECP256K1
+import org.apache.tuweni.devp2p.EthereumNodeRecord
+import org.apache.tuweni.devp2p.v5.internal.DefaultUdpConnector
+import org.apache.tuweni.devp2p.v5.packet.RandomMessage
+import org.apache.tuweni.io.Base64URLSafe
+import java.net.InetSocketAddress
+import java.time.Instant
+import kotlin.coroutines.CoroutineContext
+
+/**
+ * Service executes network discovery, according to discv5 specification
+ * (https://github.com/ethereum/devp2p/blob/master/discv5/discv5.md)
+ */
+interface NodeDiscoveryService {
+
+ /**
+ * Initializes node discovery
+ */
+ fun start()
+
+ /**
+ * Executes service shut down
+ */
+ fun terminate(await: Boolean = false)
+
+}
+
+internal class DefaultNodeDiscoveryService(
+ private val keyPair: SECP256K1.KeyPair,
+ private val localPort: Int,
+ private val bindAddress: InetSocketAddress = InetSocketAddress(localPort),
+ private val bootstrapENRList: List<String> = emptyList(),
+ private val enrSeq: Long = Instant.now().toEpochMilli(),
+ private val selfENR: Bytes = EthereumNodeRecord.toRLP(
+ keyPair,
+ enrSeq,
+ emptyMap(),
+ bindAddress.address,
+ null,
+ bindAddress.port
+ ),
+ private val connector: UdpConnector = DefaultUdpConnector(bindAddress, keyPair, selfENR),
+ override val coroutineContext: CoroutineContext = Dispatchers.Default
+) : NodeDiscoveryService, CoroutineScope {
+
+ override fun start() {
+ connector.start()
+ launch { bootstrap() }
+ }
+
+ override fun terminate(await: Boolean) {
+ runBlocking {
+ val job = async { connector.terminate() }
+ if (await) {
+ job.await()
+ }
+ }
+ }
+
+ private fun bootstrap() {
+ bootstrapENRList.forEach {
+ if (it.startsWith("enr:")) {
+ val encodedEnr = it.substringAfter("enr:")
+ val rlpENR = Base64URLSafe.decode(encodedEnr)
+ val enr = EthereumNodeRecord.fromRLP(rlpENR)
+
+ val randomMessage = RandomMessage()
+ val address = InetSocketAddress(enr.ip(), enr.udp())
+
+ connector.addPendingNodeId(address, rlpENR)
+ connector.send(address, randomMessage, rlpENR)
+ }
+ }
+ }
+}
diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/PacketCodec.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/PacketCodec.kt
new file mode 100644
index 0000000..eec5073
--- /dev/null
+++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/PacketCodec.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.v5
+
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.devp2p.v5.packet.UdpMessage
+import org.apache.tuweni.devp2p.v5.misc.DecodeResult
+import org.apache.tuweni.devp2p.v5.misc.HandshakeInitParameters
+
+/**
+ * Message reader/writer. It encodes and decodes messages, structured like at schema below
+ *
+ * tag || auth_tag || message
+ *
+ * tag || auth_header || message
+ *
+ * magic || message
+ *
+ * It also responsible for encryption functionality, so handlers receives raw messages for processing
+ */
+interface PacketCodec {
+
+ /**
+ * Encodes message, encrypting it's body
+ *
+ * @param message message for encoding
+ * @param destNodeId receiver node identifier for tag creation
+ * @param handshakeParams optional handshake parameter, if it is required to initialize handshake
+ *
+ * @return encoded message
+ */
+ fun encode(message: UdpMessage, destNodeId: Bytes, handshakeParams: HandshakeInitParameters? = null): Bytes
+
+ /**
+ * Decodes message, decrypting it's body
+ *
+ * @param message message for decoding
+ *
+ * @return decoding result, including sender identifier and decoded message
+ */
+ fun decode(message: Bytes): DecodeResult
+}
diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/UdpConnector.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/UdpConnector.kt
new file mode 100644
index 0000000..9770885
--- /dev/null
+++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/UdpConnector.kt
@@ -0,0 +1,100 @@
+/*
+ * 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.v5
+
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.crypto.SECP256K1
+import org.apache.tuweni.devp2p.v5.packet.UdpMessage
+import org.apache.tuweni.devp2p.v5.misc.HandshakeInitParameters
+import java.net.InetSocketAddress
+
+/**
+ * Module, used for network communication. It accepts and sends incoming messages and also provides peer information,
+ * like node's ENR, key pair
+ */
+interface UdpConnector {
+
+ /**
+ * Bootstraps receive loop for incoming message handling
+ */
+ fun start()
+
+ /**
+ * Shut downs both udp receive loop and sender socket
+ */
+ fun terminate()
+
+ /**
+ * Sends udp message by socket address
+ *
+ * @param address receiver address
+ * @param message message to send
+ * @param destNodeId destination node identifier
+ * @param handshakeParams optional parameter to create handshake
+ */
+ fun send(
+ address: InetSocketAddress,
+ message: UdpMessage,
+ destNodeId: Bytes,
+ handshakeParams: HandshakeInitParameters? = null
+ )
+
+ /**
+ * Gives information about connector, whether receive channel is working
+ *
+ * @return availability information
+ */
+ fun available(): Boolean
+
+ /**
+ * Gives information about connector, whether receive loop is working
+ *
+ * @return availability information
+ */
+ fun started(): Boolean
+
+ /**
+ * Add node identifier which awaits for authentication
+ *
+ * @param address socket address
+ * @param nodeId node identifier
+ */
+ fun addPendingNodeId(address: InetSocketAddress, nodeId: Bytes)
+
+ /**
+ * Get node identifier which awaits for authentication
+ *
+ * @param address socket address
+ *
+ * @return node identifier
+ */
+ fun getPendingNodeIdByAddress(address: InetSocketAddress): Bytes
+
+ /**
+ * Provides node's key pair
+ *
+ * @return node's key pair
+ */
+ fun getNodeKeyPair(): SECP256K1.KeyPair
+
+ /**
+ * Provides node's ENR
+ *
+ * @return node's ENR
+ */
+ fun getEnr(): Bytes
+}
diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/encrypt/AES128GCM.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/encrypt/AES128GCM.kt
new file mode 100644
index 0000000..f1a22ae
--- /dev/null
+++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/encrypt/AES128GCM.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.v5.encrypt
+
+import org.apache.tuweni.bytes.Bytes
+import java.nio.ByteBuffer
+import javax.crypto.Cipher
+import javax.crypto.spec.GCMParameterSpec
+import javax.crypto.spec.SecretKeySpec
+
+/**
+ * Util dedicated for AES-GCM encoding with key size equal 16 bytes
+ */
+object AES128GCM {
+
+ private const val ALGO_NAME: String = "AES"
+ private const val CIPHER_NAME: String = "AES/GCM/NoPadding"
+ private const val KEY_SIZE: Int = 128
+
+
+ /**
+ * AES128GCM encryption function
+ *
+ * @param key 16-byte encryption key
+ * @param nonce initialization vector
+ * @param message content for encryption
+ * @param data encryption metadata
+ */
+ fun encrypt(key: Bytes, nonce: Bytes, message: Bytes, data: Bytes): Bytes {
+ val nonceBytes = nonce.toArray()
+ val keySpec = SecretKeySpec(key.toArray(), ALGO_NAME)
+ val cipher = Cipher.getInstance(CIPHER_NAME)
+ val parameterSpec = GCMParameterSpec(KEY_SIZE, nonceBytes)
+
+ cipher.init(Cipher.ENCRYPT_MODE, keySpec, parameterSpec)
+
+ cipher.updateAAD(data.toArray())
+
+ val encryptedText = Bytes.wrap(cipher.doFinal(message.toArray()))
+
+ val wrappedNonce = Bytes.wrap(nonceBytes)
+ val nonceSize = Bytes.ofUnsignedInt(nonceBytes.size.toLong())
+ return Bytes.wrap(nonceSize, wrappedNonce, encryptedText)
+ }
+
+ /**
+ * AES128GCM decryption function
+ *
+ * @param encryptedContent content for decryption
+ * @param key 16-byte encryption key
+ * @param data encryption metadata
+ */
+ fun decrypt(encryptedContent: Bytes, key: Bytes, data: Bytes): Bytes {
+ val buffer = ByteBuffer.wrap(encryptedContent.toArray())
+ val nonceLength = buffer.int
+ val nonce = ByteArray(nonceLength)
+ buffer.get(nonce)
+ val encryptedText = ByteArray(buffer.remaining())
+ buffer.get(encryptedText)
+
+ val keySpec = SecretKeySpec(key.toArray(), ALGO_NAME)
+
+ val parameterSpec = GCMParameterSpec(KEY_SIZE, nonce)
+ val cipher = Cipher.getInstance(CIPHER_NAME)
+ cipher.init(Cipher.DECRYPT_MODE, keySpec, parameterSpec)
+
+ cipher.updateAAD(data.toArray())
+
+ return Bytes.wrap(cipher.doFinal(encryptedText))
+ }
+}
diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/encrypt/SessionKeyGenerator.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/encrypt/SessionKeyGenerator.kt
new file mode 100644
index 0000000..cb21279
--- /dev/null
+++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/encrypt/SessionKeyGenerator.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.v5.encrypt
+
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.devp2p.v5.misc.SessionKey
+import org.bouncycastle.crypto.digests.SHA256Digest
+import org.bouncycastle.crypto.generators.HKDFBytesGenerator
+import org.bouncycastle.crypto.params.HKDFParameters
+
+/**
+ * Generates session keys on handshake, using HKDF key derivation function
+ */
+object SessionKeyGenerator {
+
+ private const val DERIVED_KEY_SIZE: Int = 16
+
+ private val INFO_PREFIX = Bytes.wrap("discovery v5 key agreement".toByteArray())
+
+ /**
+ * Executes session keys generation
+ *
+ * @param srcNodeId sender node identifier
+ * @param destNodeId receiver node identifier
+ * @param secret the input keying material or seed
+ * @param idNonce nonce used as salt
+ */
+ fun generate(srcNodeId: Bytes, destNodeId: Bytes, secret: Bytes, idNonce: Bytes): SessionKey {
+ val info = Bytes.wrap(INFO_PREFIX, srcNodeId, destNodeId)
+
+ val hkdf = HKDFBytesGenerator(SHA256Digest())
+ val params = HKDFParameters(secret.toArray(), idNonce.toArray(), info.toArray())
+ hkdf.init(params)
+ return SessionKey(derive(hkdf), derive(hkdf), derive(hkdf))
+ }
+
+ private fun derive(hkdf: HKDFBytesGenerator): Bytes {
+ val result = ByteArray(DERIVED_KEY_SIZE)
+ hkdf.generateBytes(result, 0, result.size)
+ return Bytes.wrap(result)
+ }
+}
diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/internal/DefaultAuthenticationProvider.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/internal/DefaultAuthenticationProvider.kt
new file mode 100644
index 0000000..f03b63b
--- /dev/null
+++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/internal/DefaultAuthenticationProvider.kt
@@ -0,0 +1,133 @@
+/*
+ * 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.v5.internal
+
+import com.google.common.cache.Cache
+import com.google.common.cache.CacheBuilder
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.crypto.Hash
+import org.apache.tuweni.crypto.SECP256K1
+import org.apache.tuweni.devp2p.ENR_REQUEST_RETRY_DELAY_MS
+import org.apache.tuweni.devp2p.EthereumNodeRecord
+import org.apache.tuweni.devp2p.v5.AuthenticationProvider
+import org.apache.tuweni.devp2p.v5.encrypt.AES128GCM
+import org.apache.tuweni.devp2p.v5.encrypt.SessionKeyGenerator
+import org.apache.tuweni.devp2p.v5.misc.AuthHeader
+import org.apache.tuweni.devp2p.v5.misc.HandshakeInitParameters
+import org.apache.tuweni.devp2p.v5.misc.SessionKey
+import org.apache.tuweni.rlp.RLP
+import java.util.concurrent.TimeUnit
+
+class DefaultAuthenticationProvider(
+ private val keyPair: SECP256K1.KeyPair,
+ private val enr: Bytes
+) : AuthenticationProvider {
+
+ private val sessionKeys: Cache<String, SessionKey> = CacheBuilder
+ .newBuilder()
+ .expireAfterWrite(ENR_REQUEST_RETRY_DELAY_MS, TimeUnit.MILLISECONDS)
+ .build()
+ private val nodeId: Bytes = Hash.sha2_256(enr)
+
+ override fun authenticate(handshakeParams: HandshakeInitParameters): AuthHeader {
+ // Generate ephemeral key pair
+ val ephemeralKeyPair = SECP256K1.KeyPair.random()
+ val ephemeralKey = ephemeralKeyPair.secretKey()
+
+ val destEnr = EthereumNodeRecord.fromRLP(handshakeParams.destEnr)
+ val destNodeId = Hash.sha2_256(handshakeParams.destEnr)
+
+ // Perform agreement
+ val secret = SECP256K1.calculateKeyAgreement(ephemeralKey, destEnr.publicKey())
+
+ // Derive keys
+ val sessionKey = SessionKeyGenerator.generate(nodeId, destNodeId, secret, handshakeParams.idNonce)
+
+ setSessionKey(destNodeId.toHexString(), sessionKey)
+
+ val signature = sign(keyPair, handshakeParams)
+
+ return generateAuthHeader(enr, signature, handshakeParams, sessionKey.authRespKey, ephemeralKeyPair.publicKey())
+ }
+
+ override fun findSessionKey(nodeId: String): SessionKey? {
+ return sessionKeys.getIfPresent(nodeId)
+ }
+
+ override fun setSessionKey(nodeId: String, sessionKey: SessionKey) {
+ sessionKeys.put(nodeId, sessionKey)
+ }
+
+ override fun finalizeHandshake(senderNodeId: Bytes, authHeader: AuthHeader) {
+ val ephemeralPublicKey = SECP256K1.PublicKey.fromBytes(authHeader.ephemeralPublicKey)
+ val secret = SECP256K1.calculateKeyAgreement(keyPair.secretKey(), ephemeralPublicKey)
+
+ val sessionKey = SessionKeyGenerator.generate(senderNodeId, nodeId, secret, authHeader.idNonce)
+
+ val decryptedAuthResponse = AES128GCM.decrypt(authHeader.authResponse, sessionKey.authRespKey, Bytes.EMPTY)
+ RLP.decodeList(Bytes.wrap(decryptedAuthResponse)) { reader ->
+ reader.skipNext()
+ val signatureBytes = reader.readValue()
+ val enrRLP = reader.readValue()
+ val enr = EthereumNodeRecord.fromRLP(enrRLP)
+ val publicKey = enr.publicKey()
+ val signatureVerified = verifySignature(signatureBytes, authHeader.idNonce, publicKey)
+ if (!signatureVerified) {
+ throw IllegalArgumentException("Signature is not verified")
+ }
+ setSessionKey(senderNodeId.toHexString(), sessionKey)
+ }
+ }
+
+ private fun sign(keyPair: SECP256K1.KeyPair, params: HandshakeInitParameters): SECP256K1.Signature {
+ val signValue = Bytes.wrap(DISCOVERY_ID_NONCE, params.idNonce)
+ val hashedSignValue = Hash.sha2_256(signValue)
+ return SECP256K1.sign(hashedSignValue, keyPair)
+ }
+
+ private fun verifySignature(signatureBytes: Bytes, idNonce: Bytes, publicKey: SECP256K1.PublicKey): Boolean {
+ val signature = SECP256K1.Signature.fromBytes(signatureBytes)
+ val signValue = Bytes.wrap(DISCOVERY_ID_NONCE, idNonce)
+ val hashedSignValue = Hash.sha2_256(signValue)
+ return SECP256K1.verify(hashedSignValue, signature, publicKey)
+ }
+
+ private fun generateAuthHeader(
+ enr: Bytes,
+ signature: SECP256K1.Signature,
+ params: HandshakeInitParameters,
+ authRespKey: Bytes,
+ ephemeralPubKey: SECP256K1.PublicKey
+ ): AuthHeader {
+ val plain = RLP.encodeList { writer ->
+ writer.writeInt(VERSION)
+ writer.writeValue(signature.bytes())
+ writer.writeValue(enr) // TODO: Seq number if enrSeq from WHOAREYOU is equal to local, else nothing
+ }
+ val zeroNonce = Bytes.wrap(ByteArray(ZERO_NONCE_SIZE))
+ val authResponse = AES128GCM.encrypt(authRespKey, zeroNonce, plain, Bytes.EMPTY)
+
+ return AuthHeader(params.authTag, params.idNonce, ephemeralPubKey.bytes(), Bytes.wrap(authResponse))
+ }
+
+ companion object {
+ private const val ZERO_NONCE_SIZE: Int = 12
+ private const val VERSION: Int = 5
+
+ private val DISCOVERY_ID_NONCE: Bytes = Bytes.wrap("discovery-id-nonce".toByteArray())
+ }
+}
diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/internal/DefaultPacketCodec.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/internal/DefaultPacketCodec.kt
new file mode 100644
index 0000000..6dbab0f
--- /dev/null
+++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/internal/DefaultPacketCodec.kt
@@ -0,0 +1,125 @@
+/*
+ * 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.v5.internal
+
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.crypto.Hash
+import org.apache.tuweni.crypto.SECP256K1
+import org.apache.tuweni.devp2p.v5.AuthenticationProvider
+import org.apache.tuweni.devp2p.v5.PacketCodec
+import org.apache.tuweni.devp2p.v5.encrypt.AES128GCM
+import org.apache.tuweni.devp2p.v5.packet.FindNodeMessage
+import org.apache.tuweni.devp2p.v5.packet.RandomMessage
+import org.apache.tuweni.devp2p.v5.packet.UdpMessage
+import org.apache.tuweni.devp2p.v5.packet.WhoAreYouMessage
+import org.apache.tuweni.devp2p.v5.misc.AuthHeader
+import org.apache.tuweni.devp2p.v5.misc.DecodeResult
+import org.apache.tuweni.devp2p.v5.misc.HandshakeInitParameters
+import org.apache.tuweni.rlp.RLP
+import org.apache.tuweni.rlp.RLPReader
+import kotlin.IllegalArgumentException
+
+class DefaultPacketCodec(
+ private val keyPair: SECP256K1.KeyPair,
+ private val enr: Bytes,
+ private val nodeId: Bytes = Hash.sha2_256(enr),
+ private val authenticationProvider: AuthenticationProvider = DefaultAuthenticationProvider(keyPair, enr)
+) : PacketCodec {
+
+ override fun encode(message: UdpMessage, destNodeId: Bytes, handshakeParams: HandshakeInitParameters?): Bytes {
+ if (message is WhoAreYouMessage) {
+ val magic = UdpMessage.magic(nodeId)
+ val content = message.encode()
+ return Bytes.wrap(magic, content)
+ }
+
+ val tag = UdpMessage.tag(nodeId, destNodeId)
+ if (message is RandomMessage) {
+ val authTag = UdpMessage.authTag()
+ val rlpAuthTag = RLP.encodeValue(authTag)
+ val content = message.encode()
+ return Bytes.wrap(tag, rlpAuthTag, content)
+ }
+
+ val authHeader = handshakeParams?.let { authenticationProvider.authenticate(handshakeParams) }
+
+ val initiatorKey = authenticationProvider.findSessionKey(destNodeId.toHexString())?.initiatorKey
+ ?: throw IllegalArgumentException() // TODO handle
+ val messagePlain = Bytes.wrap(message.getMessageType(), message.encode())
+ return if (null != authHeader) {
+ val encodedHeader = authHeader.asRlp()
+ val authTag = authHeader.authTag
+ val encryptionMeta = Bytes.wrap(tag, encodedHeader)
+ val encryptionResult = AES128GCM.encrypt(initiatorKey, authTag, messagePlain, encryptionMeta)
+ Bytes.wrap(tag, encodedHeader, encryptionResult)
+ } else {
+ val authTag = UdpMessage.authTag()
+ val authTagHeader = RLP.encodeValue(authTag)
+ val encryptionResult = AES128GCM.encrypt(initiatorKey, authTag, messagePlain, tag)
+ Bytes.wrap(tag, authTagHeader, encryptionResult)
+ }
+ }
+
+ override fun decode(message: Bytes): DecodeResult {
+ val tag = message.slice(0, UdpMessage.TAG_LENGTH)
+ val senderNodeId = UdpMessage.getSourceFromTag(tag, nodeId)
+ val contentWithHeader = message.slice(UdpMessage.TAG_LENGTH)
+ val decodedMessage = RLP.decode(contentWithHeader) { reader -> read(tag, senderNodeId, contentWithHeader, reader) }
+ return DecodeResult(senderNodeId, decodedMessage)
+ }
+
+ private fun read(tag: Bytes, senderNodeId: Bytes, contentWithHeader: Bytes, reader: RLPReader): UdpMessage {
+ // Distinguish auth header or auth tag
+ var authHeader: AuthHeader? = null
+ if (reader.nextIsList()) {
+ if (WHO_ARE_YOU_MESSAGE_LENGTH == contentWithHeader.size()) {
+ return WhoAreYouMessage.create(contentWithHeader)
+ }
+ authHeader = reader.readList { listReader ->
+ val authenticationTag = listReader.readValue()
+ val idNonce = listReader.readValue()
+ val authScheme = listReader.readString()
+ val ephemeralPublicKey = listReader.readValue()
+ val authResponse = listReader.readValue()
+ return@readList AuthHeader(authenticationTag, idNonce, ephemeralPublicKey, authResponse, authScheme)
+ }
+ authenticationProvider.finalizeHandshake(senderNodeId, authHeader)
+ } else {
+ reader.readValue()
+ }
+
+ val encryptedContent = contentWithHeader.slice(reader.position())
+
+ // Decrypt
+ val decryptionKey = authenticationProvider.findSessionKey(senderNodeId.toHexString())?.initiatorKey
+ ?: return RandomMessage(encryptedContent)
+ val decryptMetadata = authHeader?.let { Bytes.wrap(tag, authHeader.asRlp()) } ?: tag
+ val decryptedContent = AES128GCM.decrypt(encryptedContent, decryptionKey, decryptMetadata)
+ val messageType = decryptedContent.slice(0, Byte.SIZE_BYTES)
+ val message = decryptedContent.slice(Byte.SIZE_BYTES)
+
+ // Retrieve result
+ return when (messageType.toInt()) {
+ 3 -> FindNodeMessage.create(message)
+ else -> throw IllegalArgumentException("Unknown message retrieved")
+ }
+ }
+
+ companion object {
+ private const val WHO_ARE_YOU_MESSAGE_LENGTH = 48
+ }
+}
diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/internal/DefaultUdpConnector.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/internal/DefaultUdpConnector.kt
new file mode 100644
index 0000000..ebb4eb8
--- /dev/null
+++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/internal/DefaultUdpConnector.kt
@@ -0,0 +1,128 @@
+/*
+ * 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.v5.internal
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.crypto.Hash
+import org.apache.tuweni.crypto.SECP256K1
+import org.apache.tuweni.devp2p.v5.MessageHandler
+import org.apache.tuweni.devp2p.v5.PacketCodec
+import org.apache.tuweni.devp2p.v5.UdpConnector
+import org.apache.tuweni.devp2p.v5.internal.handler.RandomMessageHandler
+import org.apache.tuweni.devp2p.v5.internal.handler.WhoAreYouMessageHandler
+import org.apache.tuweni.devp2p.v5.packet.FindNodeMessage
+import org.apache.tuweni.devp2p.v5.packet.RandomMessage
+import org.apache.tuweni.devp2p.v5.packet.UdpMessage
+import org.apache.tuweni.devp2p.v5.packet.WhoAreYouMessage
+import org.apache.tuweni.devp2p.v5.misc.HandshakeInitParameters
+import org.apache.tuweni.net.coroutines.CoroutineDatagramChannel
+import java.net.InetSocketAddress
+import java.nio.ByteBuffer
+import java.util.logging.Logger
+import kotlin.coroutines.CoroutineContext
+
+class DefaultUdpConnector(
+ private val bindAddress: InetSocketAddress,
+ private val keyPair: SECP256K1.KeyPair,
+ private val selfEnr: Bytes,
+ private val nodeId: Bytes = Hash.sha2_256(selfEnr),
+ private val receiveChannel: CoroutineDatagramChannel = CoroutineDatagramChannel.open(),
+ private val sendChannel: CoroutineDatagramChannel = CoroutineDatagramChannel.open(),
+ private val packetCodec: PacketCodec = DefaultPacketCodec(keyPair, selfEnr),
+ override val coroutineContext: CoroutineContext = Dispatchers.IO
+) : UdpConnector, CoroutineScope {
+
+ private val log: Logger = Logger.getLogger(this.javaClass.simpleName)
+
+ private val randomMessageHandler: MessageHandler<RandomMessage> = RandomMessageHandler()
+ private val whoAreYouMessageHandler: MessageHandler<WhoAreYouMessage> = WhoAreYouMessageHandler(nodeId)
+
+ private val authenticatingPeers: MutableMap<InetSocketAddress, Bytes> = mutableMapOf()
+
+ private lateinit var receiveJob: Job
+
+ override fun start() {
+ receiveChannel.bind(bindAddress)
+
+ receiveJob = launch {
+ val datagram = ByteBuffer.allocate(UdpMessage.MAX_UDP_MESSAGE_SIZE)
+ while (receiveChannel.isOpen) {
+ datagram.clear()
+ val address = receiveChannel.receive(datagram) as InetSocketAddress
+ datagram.flip()
+ try {
+ processDatagram(datagram, address)
+ } catch (ex: Exception) {
+ log.warning(ex.message)
+ }
+ }
+ }
+ }
+
+ override fun send(
+ address: InetSocketAddress,
+ message: UdpMessage,
+ destNodeId: Bytes,
+ handshakeParams: HandshakeInitParameters?
+ ) {
+ launch {
+ val buffer = packetCodec.encode(message, destNodeId, handshakeParams)
+ sendChannel.send(ByteBuffer.wrap(buffer.toArray()), address)
+ }
+ }
+
+ override fun terminate() {
+ receiveJob.cancel()
+ receiveChannel.close()
+ sendChannel.close()
+ }
+
+ override fun available(): Boolean = receiveChannel.isOpen
+
+ override fun started(): Boolean = ::receiveJob.isInitialized && available()
+
+ override fun getEnr(): Bytes = selfEnr
+
+ override fun addPendingNodeId(address: InetSocketAddress, nodeId: Bytes) {
+ authenticatingPeers[address] = nodeId
+ }
+
+ override fun getNodeKeyPair(): SECP256K1.KeyPair = keyPair
+
+ override fun getPendingNodeIdByAddress(address: InetSocketAddress): Bytes {
+ val result = authenticatingPeers[address]
+ ?: throw IllegalArgumentException("Authenticated peer not found with address ${address.hostName}:${address.port}")
+ authenticatingPeers.remove(address)
+ return result
+ }
+
+ private fun processDatagram(datagram: ByteBuffer, address: InetSocketAddress) {
+ val messageBytes = Bytes.wrapByteBuffer(datagram)
+ val decodeResult = packetCodec.decode(messageBytes)
+ val message = decodeResult.message
+ when (message) {
+ is RandomMessage -> randomMessageHandler.handle(message, address, decodeResult.srcNodeId, this)
+ is WhoAreYouMessage -> whoAreYouMessageHandler.handle(message, address, decodeResult.srcNodeId, this)
+ is FindNodeMessage -> { } //TODO: response with NODES message
+ else -> throw IllegalArgumentException("Unexpected message has been received - ${message::class.java.simpleName}")
+ }
+ }
+}
diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/internal/handler/RandomMessageHandler.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/internal/handler/RandomMessageHandler.kt
new file mode 100644
index 0000000..477710a
--- /dev/null
+++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/internal/handler/RandomMessageHandler.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.v5.internal.handler
+
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.devp2p.v5.MessageHandler
+import org.apache.tuweni.devp2p.v5.UdpConnector
+import org.apache.tuweni.devp2p.v5.packet.RandomMessage
+import org.apache.tuweni.devp2p.v5.packet.WhoAreYouMessage
+import java.net.InetSocketAddress
+
+class RandomMessageHandler : MessageHandler<RandomMessage> {
+
+ override fun handle(message: RandomMessage, address: InetSocketAddress, srcNodeId: Bytes, connector: UdpConnector) {
+ connector.addPendingNodeId(address, srcNodeId)
+ val response = WhoAreYouMessage()
+ connector.send(address, response, srcNodeId)
+ }
+}
diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/internal/handler/WhoAreYouMessageHandler.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/internal/handler/WhoAreYouMessageHandler.kt
new file mode 100644
index 0000000..754ceae
--- /dev/null
+++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/internal/handler/WhoAreYouMessageHandler.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.v5.internal.handler
+
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.crypto.Hash
+import org.apache.tuweni.devp2p.v5.MessageHandler
+import org.apache.tuweni.devp2p.v5.UdpConnector
+import org.apache.tuweni.devp2p.v5.packet.FindNodeMessage
+import org.apache.tuweni.devp2p.v5.packet.WhoAreYouMessage
+import org.apache.tuweni.devp2p.v5.misc.HandshakeInitParameters
+import java.net.InetSocketAddress
+
+class WhoAreYouMessageHandler(
+ private val nodeId: Bytes
+) : MessageHandler<WhoAreYouMessage> {
+
+ override fun handle(
+ message: WhoAreYouMessage,
+ address: InetSocketAddress,
+ srcNodeId: Bytes,
+ connector: UdpConnector
+ ) {
+ // Retrieve enr
+ val destRlp = connector.getPendingNodeIdByAddress(address)
+ val handshakeParams = HandshakeInitParameters(message.idNonce, message.authTag, destRlp)
+ val destNodeId = Hash.sha2_256(destRlp)
+
+ val findNodeMessage = FindNodeMessage()
+ connector.send(address, findNodeMessage, destNodeId, handshakeParams)
+ }
+}
diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/misc/AuthHeader.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/misc/AuthHeader.kt
new file mode 100644
index 0000000..0996d9f
--- /dev/null
+++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/misc/AuthHeader.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.v5.misc
+
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.rlp.RLP
+
+class AuthHeader(
+ val authTag: Bytes,
+ val idNonce: Bytes,
+ val ephemeralPublicKey: Bytes,
+ val authResponse: Bytes,
+ val authScheme: String = AUTH_SCHEME
+) {
+
+ fun asRlp(): Bytes {
+ return RLP.encodeList { writer ->
+ writer.writeValue(authTag)
+ writer.writeValue(idNonce)
+ writer.writeValue(AUTH_SCHEME_BYTES)
+ writer.writeValue(ephemeralPublicKey)
+ writer.writeValue(authResponse)
+ }
+ }
+
+ companion object {
+ private const val AUTH_SCHEME: String = "gcm"
+
+ private val AUTH_SCHEME_BYTES: Bytes = Bytes.wrap(AUTH_SCHEME.toByteArray())
+ }
+}
diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/misc/DecodeResult.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/misc/DecodeResult.kt
new file mode 100644
index 0000000..8693e94
--- /dev/null
+++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/misc/DecodeResult.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.v5.misc
+
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.devp2p.v5.packet.UdpMessage
+
+class DecodeResult(
+ val srcNodeId: Bytes,
+ val message: UdpMessage
+)
diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/misc/HandshakeInitParameters.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/misc/HandshakeInitParameters.kt
new file mode 100644
index 0000000..c9a6bc9
--- /dev/null
+++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/misc/HandshakeInitParameters.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.v5.misc
+
+import org.apache.tuweni.bytes.Bytes
+
+class HandshakeInitParameters(
+ val idNonce: Bytes,
+ val authTag: Bytes,
+ val destEnr: Bytes
+)
diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/misc/SessionKey.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/misc/SessionKey.kt
new file mode 100644
index 0000000..8d0f94a
--- /dev/null
+++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/misc/SessionKey.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.v5.misc
+
+import org.apache.tuweni.bytes.Bytes
+
+class SessionKey(
+ val initiatorKey: Bytes,
+ val recipientKey: Bytes,
+ val authRespKey: Bytes
+)
diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/FindNodeMessage.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/FindNodeMessage.kt
new file mode 100644
index 0000000..b39ae46
--- /dev/null
+++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/FindNodeMessage.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.v5.packet
+
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.rlp.RLP
+
+class FindNodeMessage(
+ val requestId: Bytes = UdpMessage.requestId(),
+ val distance: Long = 0
+) : UdpMessage() {
+
+ private val encodedMessageType: Bytes = Bytes.fromHexString("0x03")
+
+ override fun encode(): Bytes {
+ return RLP.encodeList { writer ->
+ writer.writeValue(requestId)
+ writer.writeLong(distance)
+ }
+ }
+
+ override fun getMessageType(): Bytes = encodedMessageType
+
+ companion object {
+ fun create(content: Bytes): FindNodeMessage {
+ return RLP.decodeList(content) { reader ->
+ val requestId = reader.readValue()
+ val distance = reader.readLong()
+ return@decodeList FindNodeMessage(requestId, distance)
+ }
+ }
+ }
+}
diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/NodesMessage.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/NodesMessage.kt
new file mode 100644
index 0000000..5c213e5
--- /dev/null
+++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/NodesMessage.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.v5.packet
+
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.rlp.RLP
+
+class NodesMessage(
+ val requestId: Bytes = UdpMessage.requestId(),
+ val total: Int,
+ val nodeRecords: List<Bytes>
+) : UdpMessage() {
+
+ private val encodedMessageType: Bytes = Bytes.fromHexString("0x04")
+
+ override fun getMessageType(): Bytes = encodedMessageType
+
+ override fun encode(): Bytes {
+ return RLP.encodeList { writer ->
+ writer.writeValue(requestId)
+ writer.writeInt(total)
+ writer.writeList(nodeRecords) { listWriter, it ->
+ listWriter.writeValue(it)
+ }
+ }
+ }
+
+ companion object {
+ fun create(content: Bytes): NodesMessage {
+ return RLP.decodeList(content) { reader ->
+ val requestId = reader.readValue()
+ val total = reader.readInt()
+ val nodeRecords = reader.readListContents { listReader ->
+ listReader.readValue()
+ }
+ return@decodeList NodesMessage(requestId, total, nodeRecords)
+ }
+ }
+ }
+}
diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/PingMessage.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/PingMessage.kt
new file mode 100644
index 0000000..c9cc06a
--- /dev/null
+++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/PingMessage.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.v5.packet
+
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.rlp.RLP
+
+class PingMessage(
+ val requestId: Bytes = UdpMessage.requestId(),
+ val enrSeq: Long = 0
+) : UdpMessage() {
+
+ private val encodedMessageType: Bytes = Bytes.fromHexString("0x01")
+
+ override fun getMessageType(): Bytes = encodedMessageType
+
+ override fun encode(): Bytes {
+ return RLP.encodeList { reader ->
+ reader.writeValue(requestId)
+ reader.writeLong(enrSeq)
+ }
+ }
+
+ companion object {
+ fun create(content: Bytes): PingMessage {
+ return RLP.decodeList(content) { reader ->
+ val requestId = reader.readValue()
+ val enrSeq = reader.readLong()
+ return@decodeList PingMessage(requestId, enrSeq)
+ }
+ }
+ }
+}
diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/PongMessage.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/PongMessage.kt
new file mode 100644
index 0000000..0323853
--- /dev/null
+++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/PongMessage.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.v5.packet
+
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.rlp.RLP
+import java.net.InetAddress
+
+class PongMessage(
+ val requestId: Bytes = UdpMessage.requestId(),
+ val enrSeq: Long = 0,
+ val recipientIp: InetAddress,
+ val recipientPort: Int
+) : UdpMessage() {
+
+ private val encodedMessageType: Bytes = Bytes.fromHexString("0x02")
+
+ override fun getMessageType(): Bytes = encodedMessageType
+
+ override fun encode(): Bytes {
+ return RLP.encodeList { writer ->
+ writer.writeValue(requestId)
+ writer.writeLong(enrSeq)
+
+ val bytesIp = Bytes.wrap(recipientIp.address)
+ writer.writeValue(bytesIp)
+ writer.writeInt(recipientPort)
+ }
+ }
+
+ companion object {
+ fun create(content: Bytes): PongMessage {
+ return RLP.decodeList(content) { reader ->
+ val requestId = reader.readValue()
+ val enrSeq = reader.readLong()
+ val address = InetAddress.getByAddress(reader.readValue().toArray())
+ val recipientPort = reader.readInt()
+ return@decodeList PongMessage(requestId, enrSeq, address, recipientPort)
+ }
+ }
+ }
+}
diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/RandomMessage.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/RandomMessage.kt
new file mode 100644
index 0000000..e43ded7
--- /dev/null
+++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/RandomMessage.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.v5.packet
+
+import org.apache.tuweni.bytes.Bytes
+
+class RandomMessage(
+ val data: Bytes = randomData()
+) : UdpMessage() {
+
+ override fun encode(): Bytes {
+ return data
+ }
+
+ companion object {
+ fun create(content: Bytes): RandomMessage {
+ return RandomMessage(content)
+ }
+ }
+}
diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/RegConfirmationMessage.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/RegConfirmationMessage.kt
new file mode 100644
index 0000000..bd643dd
--- /dev/null
+++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/RegConfirmationMessage.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.v5.packet
+
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.rlp.RLP
+
+class RegConfirmationMessage(
+ val requestId: Bytes = UdpMessage.requestId(),
+ val registered: Boolean = true
+) : UdpMessage() {
+
+ private val encodedMessageType: Bytes = Bytes.fromHexString("0x08")
+
+ override fun getMessageType(): Bytes = encodedMessageType
+
+ override fun encode(): Bytes {
+ return RLP.encodeList { writer ->
+ writer.writeValue(requestId)
+ writer.writeByte(if (registered) 1 else 0)
+ }
+ }
+
+ companion object {
+ fun create(content: Bytes): RegConfirmationMessage {
+ return RLP.decodeList(content) { reader ->
+ val requestId = reader.readValue()
+ val registered = (reader.readByte() == 1.toByte())
+ return@decodeList RegConfirmationMessage(requestId, registered)
+ }
+ }
+ }
+}
diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/RegTopicMessage.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/RegTopicMessage.kt
new file mode 100644
index 0000000..afe2c23
--- /dev/null
+++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/RegTopicMessage.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.v5.packet
+
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.rlp.RLP
+
+class RegTopicMessage(
+ val requestId: Bytes = UdpMessage.requestId(),
+ val ticket: Bytes,
+ val nodeRecord: Bytes
+) : UdpMessage() {
+
+ private val encodedMessageType: Bytes = Bytes.fromHexString("0x07")
+
+ override fun getMessageType(): Bytes = encodedMessageType
+
+ override fun encode(): Bytes {
+ return RLP.encodeList { writer ->
+ writer.writeValue(requestId)
+ writer.writeValue(ticket)
+ writer.writeValue(nodeRecord)
+ }
+ }
+
+ companion object {
+ fun create(content: Bytes): RegTopicMessage {
+ return RLP.decodeList(content) { reader ->
+ val requestId = reader.readValue()
+ val ticket = reader.readValue()
+ val nodeRecord = reader.readValue()
+ return@decodeList RegTopicMessage(requestId, ticket, nodeRecord)
+ }
+ }
+ }
+}
diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/ReqTicketMessage.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/ReqTicketMessage.kt
new file mode 100644
index 0000000..0381a85
--- /dev/null
+++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/ReqTicketMessage.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.v5.packet
+
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.rlp.RLP
+
+class ReqTicketMessage(
+ val requestId: Bytes = UdpMessage.requestId(),
+ val topic: Bytes
+) : UdpMessage() {
+
+ private val encodedMessageType: Bytes = Bytes.fromHexString("0x05")
+
+ override fun getMessageType(): Bytes = encodedMessageType
+
+ override fun encode(): Bytes {
+ return RLP.encodeList { writer ->
+ writer.writeValue(requestId)
+ writer.writeValue(topic)
+ }
+ }
+
+ companion object {
+ fun create(content: Bytes): ReqTicketMessage {
+ return RLP.decodeList(content) { reader ->
+ val requestId = reader.readValue()
+ val topic = reader.readValue()
+ return@decodeList ReqTicketMessage(requestId, topic)
+ }
+ }
+ }
+}
diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/TicketMessage.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/TicketMessage.kt
new file mode 100644
index 0000000..8d628f2
--- /dev/null
+++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/TicketMessage.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.v5.packet
+
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.rlp.RLP
+
+class TicketMessage(
+ val requestId: Bytes = UdpMessage.requestId(),
+ val ticket: Bytes,
+ val waitTime: Long
+) : UdpMessage() {
+
+ private val encodedMessageType: Bytes = Bytes.fromHexString("0x06")
+
+ override fun getMessageType(): Bytes = encodedMessageType
+
+ override fun encode(): Bytes {
+ return RLP.encodeList { writer ->
+ writer.writeValue(requestId)
+ writer.writeValue(ticket)
+ writer.writeLong(waitTime)
+ }
+ }
+
+ companion object {
+ fun create(content: Bytes): TicketMessage {
+ return RLP.decodeList(content) { reader ->
+ val requestId = reader.readValue()
+ val ticket = reader.readValue()
+ val waitTime = reader.readLong()
+ return@decodeList TicketMessage(requestId, ticket, waitTime)
+ }
+ }
+ }
+}
diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/TopicQueryMessage.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/TopicQueryMessage.kt
new file mode 100644
index 0000000..14736ff
--- /dev/null
+++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/TopicQueryMessage.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.v5.packet
+
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.rlp.RLP
+
+class TopicQueryMessage(
+ val requestId: Bytes = UdpMessage.requestId(),
+ val topic: Bytes
+) : UdpMessage() {
+
+ private val encodedMessageType: Bytes = Bytes.fromHexString("0x09")
+
+ override fun getMessageType(): Bytes = encodedMessageType
+
+ override fun encode(): Bytes {
+ return RLP.encodeList { writer ->
+ writer.writeValue(requestId)
+ writer.writeValue(topic)
+ }
+ }
+
+ companion object {
+ fun create(content: Bytes): TopicQueryMessage {
+ return RLP.decodeList(content) { reader ->
+ val requestId = reader.readValue()
+ val topic = reader.readValue()
+ return@decodeList TopicQueryMessage(requestId, topic)
+ }
+ }
+ }
+}
diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/UdpMessage.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/UdpMessage.kt
new file mode 100644
index 0000000..91ae779
--- /dev/null
+++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/UdpMessage.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.v5.packet
+
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.crypto.Hash
+
+abstract class UdpMessage {
+
+ abstract fun encode(): Bytes
+
+ open fun getMessageType(): Bytes {
+ throw UnsupportedOperationException("Message don't identified with type")
+ }
+
+ companion object {
+
+ const val MAX_UDP_MESSAGE_SIZE = 1280
+ const val TAG_LENGTH: Int = 32
+ const val AUTH_TAG_LENGTH: Int = 12
+ const val RANDOM_DATA_LENGTH: Int = 44
+ const val ID_NONCE_LENGTH: Int = 32
+ const val REQUEST_ID_LENGTH: Int = 8
+
+ private val WHO_ARE_YOU: Bytes = Bytes.wrap("WHOAREYOU".toByteArray())
+
+ fun magic(dest: Bytes): Bytes {
+ val concatView = Bytes.wrap(dest, WHO_ARE_YOU)
+ return Hash.sha2_256(concatView)
+ }
+
+ fun tag(src: Bytes, dest: Bytes): Bytes {
+ val encodedDestKey = Hash.sha2_256(dest)
+ return Bytes.wrap(encodedDestKey).xor(src)
+ }
+
+ fun getSourceFromTag(tag: Bytes, dest: Bytes): Bytes {
+ val encodedDestKey = Hash.sha2_256(dest)
+ return Bytes.wrap(encodedDestKey).xor(tag)
+ }
+
+ fun requestId(): Bytes = Bytes.random(REQUEST_ID_LENGTH)
+
+ fun authTag(): Bytes = Bytes.random(AUTH_TAG_LENGTH)
+
+ fun randomData(): Bytes = Bytes.random(RANDOM_DATA_LENGTH)
+
+ fun idNonce(): Bytes = Bytes.random(ID_NONCE_LENGTH)
+ }
+}
diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/WhoAreYouMessage.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/WhoAreYouMessage.kt
new file mode 100644
index 0000000..8108294
--- /dev/null
+++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/v5/packet/WhoAreYouMessage.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.v5.packet
+
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.rlp.RLP
+
+class WhoAreYouMessage(
+ val authTag: Bytes = authTag(),
+ val idNonce: Bytes = idNonce(),
+ val enrSeq: Long = 0
+) : UdpMessage() {
+
+ override fun encode(): Bytes {
+ return RLP.encodeList { w ->
+ w.writeValue(authTag)
+ w.writeValue(idNonce)
+ w.writeLong(enrSeq)
+ }
+ }
+
+ companion object {
+ fun create(content: Bytes): WhoAreYouMessage {
+ return RLP.decodeList(content) { r ->
+ val authTag = r.readValue()
+ val idNonce = r.readValue()
+ val enrSeq = r.readLong()
+ return@decodeList WhoAreYouMessage(authTag, idNonce, enrSeq)
+ }
+ }
+ }
+}
diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/DefaultNodeDiscoveryServiceTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/DefaultNodeDiscoveryServiceTest.kt
new file mode 100644
index 0000000..a9f64e5
--- /dev/null
+++ b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/DefaultNodeDiscoveryServiceTest.kt
@@ -0,0 +1,104 @@
+/*
+ * 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.v5
+
+import kotlinx.coroutines.runBlocking
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.crypto.SECP256K1
+import org.apache.tuweni.devp2p.EthereumNodeRecord
+import org.apache.tuweni.devp2p.v5.internal.DefaultUdpConnector
+import org.apache.tuweni.devp2p.v5.packet.RandomMessage
+import org.apache.tuweni.devp2p.v5.packet.UdpMessage
+import org.apache.tuweni.io.Base64URLSafe
+import org.apache.tuweni.junit.BouncyCastleExtension
+import org.apache.tuweni.net.coroutines.CoroutineDatagramChannel
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import java.net.InetAddress
+import java.net.InetSocketAddress
+import java.nio.ByteBuffer
+import java.time.Instant
+
+@ExtendWith(BouncyCastleExtension::class)
+class DefaultNodeDiscoveryServiceTest {
+
+ private val recipientKeyPair: SECP256K1.KeyPair = SECP256K1.KeyPair.random()
+ private val recipientEnr: Bytes =
+ EthereumNodeRecord.toRLP(recipientKeyPair, ip = InetAddress.getLocalHost(), udp = 9091)
+ private val encodedEnr: String = "enr:${Base64URLSafe.encode(recipientEnr)}"
+ private val keyPair: SECP256K1.KeyPair = SECP256K1.KeyPair.random()
+ private val localPort: Int = 9090
+ private val bindAddress: InetSocketAddress = InetSocketAddress(localPort)
+ private val bootstrapENRList: List<String> = listOf(encodedEnr)
+ private val enrSeq: Long = Instant.now().toEpochMilli()
+ private val selfENR: Bytes = EthereumNodeRecord.toRLP(
+ keyPair,
+ enrSeq,
+ emptyMap(),
+ bindAddress.address,
+ null,
+ bindAddress.port
+ )
+ private val connector: UdpConnector =
+ DefaultUdpConnector(bindAddress, keyPair, selfENR)
+
+ private val nodeDiscoveryService: NodeDiscoveryService =
+ DefaultNodeDiscoveryService(
+ keyPair,
+ localPort,
+ bindAddress,
+ bootstrapENRList,
+ enrSeq,
+ selfENR,
+ connector
+ )
+
+ @Test
+ fun startInitializesConnectorAndBootstraps() {
+ val recipientSocket = CoroutineDatagramChannel.open()
+ recipientSocket.bind(InetSocketAddress(9091))
+
+ nodeDiscoveryService.start()
+
+ runBlocking {
+ val buffer = ByteBuffer.allocate(UdpMessage.MAX_UDP_MESSAGE_SIZE)
+ recipientSocket.receive(buffer)
+ buffer.flip()
+ val receivedBytes = Bytes.wrapByteBuffer(buffer)
+ val content = receivedBytes.slice(45)
+
+ val message = RandomMessage.create(content)
+ assert(message.data.size() == UdpMessage.RANDOM_DATA_LENGTH)
+ }
+
+ assert(connector.started())
+
+ recipientSocket.close()
+ nodeDiscoveryService.terminate()
+ }
+
+ @Test
+ fun terminateShutsDownService() {
+ nodeDiscoveryService.start()
+
+ assert(connector.started())
+
+ nodeDiscoveryService.terminate(true)
+
+ assert(!connector.available())
+ }
+}
diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/HandshakeIntegrationTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/HandshakeIntegrationTest.kt
new file mode 100644
index 0000000..46b0d47
--- /dev/null
+++ b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/HandshakeIntegrationTest.kt
@@ -0,0 +1,113 @@
+/*
+ * 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.v5
+
+import kotlinx.coroutines.runBlocking
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.crypto.Hash
+import org.apache.tuweni.crypto.SECP256K1
+import org.apache.tuweni.devp2p.EthereumNodeRecord
+import org.apache.tuweni.devp2p.v5.internal.DefaultAuthenticationProvider
+import org.apache.tuweni.devp2p.v5.internal.DefaultPacketCodec
+import org.apache.tuweni.devp2p.v5.internal.DefaultUdpConnector
+import org.apache.tuweni.devp2p.v5.packet.FindNodeMessage
+import org.apache.tuweni.devp2p.v5.packet.RandomMessage
+import org.apache.tuweni.devp2p.v5.packet.UdpMessage
+import org.apache.tuweni.devp2p.v5.packet.WhoAreYouMessage
+import org.apache.tuweni.io.Base64URLSafe
+import org.apache.tuweni.junit.BouncyCastleExtension
+import org.apache.tuweni.net.coroutines.CoroutineDatagramChannel
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import java.net.InetAddress
+import java.net.InetSocketAddress
+import java.nio.ByteBuffer
+
+@ExtendWith(BouncyCastleExtension::class)
+class HandshakeIntegrationTest {
+
+ private val keyPair: SECP256K1.KeyPair = SECP256K1.KeyPair.random()
+ private val enr: Bytes = EthereumNodeRecord.toRLP(keyPair, ip = InetAddress.getLocalHost(), udp = SERVICE_PORT)
+ private val address: InetSocketAddress = InetSocketAddress(InetAddress.getLocalHost(), SERVICE_PORT)
+ private val connector: UdpConnector = DefaultUdpConnector(address, keyPair, enr)
+
+ private val clientKeyPair = SECP256K1.KeyPair.random()
+ private val clientEnr = EthereumNodeRecord.toRLP(clientKeyPair, ip = InetAddress.getLocalHost(), udp = CLIENT_PORT)
+ private val authProvider = DefaultAuthenticationProvider(clientKeyPair, clientEnr)
+ private val clientCodec = DefaultPacketCodec(clientKeyPair, clientEnr, authenticationProvider = authProvider)
+ private val socket = CoroutineDatagramChannel.open()
+
+ private val clientNodeId: Bytes = Hash.sha2_256(clientEnr)
+
+ private val bootList = listOf("enr:${Base64URLSafe.encode(clientEnr)}")
+ private val service: NodeDiscoveryService =
+ DefaultNodeDiscoveryService(keyPair, SERVICE_PORT, bootstrapENRList = bootList, connector = connector)
+
+ @BeforeEach
+ fun init() {
+ socket.bind(InetSocketAddress(9091))
+ service.start()
+ }
+
+ @Test
+ fun discv5HandshakeTest() {
+ runBlocking {
+ val buffer = ByteBuffer.allocate(UdpMessage.MAX_UDP_MESSAGE_SIZE)
+ socket.receive(buffer)
+ buffer.flip()
+
+ var content = Bytes.wrapByteBuffer(buffer)
+ var decodingResult = clientCodec.decode(content)
+ assert(decodingResult.message is RandomMessage)
+ buffer.clear()
+
+ sendWhoAreYou()
+
+ socket.receive(buffer)
+ buffer.flip()
+ content = Bytes.wrapByteBuffer(buffer)
+ decodingResult = clientCodec.decode(content)
+ assert(decodingResult.message is FindNodeMessage)
+ assert(null != authProvider.findSessionKey(Hash.sha2_256(enr).toHexString()))
+
+ val message = decodingResult.message as FindNodeMessage
+
+ assert(message.distance == 0L)
+ assert(message.requestId.size() == UdpMessage.REQUEST_ID_LENGTH)
+ }
+ }
+
+ @AfterEach
+ fun tearDown() {
+ service.terminate(true)
+ socket.close()
+ }
+
+ private suspend fun sendWhoAreYou() {
+ val message = WhoAreYouMessage()
+ val bytes = clientCodec.encode(message, clientNodeId)
+ val buffer = ByteBuffer.wrap(bytes.toArray())
+ socket.send(buffer, address)
+ }
+
+ companion object {
+ private const val SERVICE_PORT: Int = 9090
+ private const val CLIENT_PORT: Int = 9091
+ }
+}
diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/encrypt/AES128GCMTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/encrypt/AES128GCMTest.kt
new file mode 100644
index 0000000..4146d05
--- /dev/null
+++ b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/encrypt/AES128GCMTest.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.v5.encrypt
+
+import org.apache.tuweni.bytes.Bytes
+import org.junit.jupiter.api.Test
+
+class AES128GCMTest {
+
+ @Test
+ fun encryptPerformsAES128GCMEncryption() {
+ val expectedResult = Bytes.fromHexString("0x000000207FC4FDB0E50ACBDA9CD993CFD3A3752104935B91F61B2AF2602C2" +
+ "DC4EFD97AFB943DAB6B1F5A0B13E83C41964F818AB8A51D6D30550BAE8B33A952AA1B6818AB88B66DBD60F5E016FA546808D983B70D")
+
+ val key = Bytes.fromHexString("0xA924872EAE2DA2C0057ED6DEBD8CAAB8")
+ val nonce = Bytes.fromHexString("0x7FC4FDB0E50ACBDA9CD993CFD3A3752104935B91F61B2AF2602C2DC4EFD97AFB")
+ val data = Bytes.fromHexString("0x19F23925525AF4C2697C1BED166EEB37B5381C10E508A27BCAA02CE661E62A2B")
+
+ val result = AES128GCM.encrypt(key, nonce, data, Bytes.EMPTY)
+
+ assert(result == expectedResult)
+ }
+
+ @Test
+ fun decryptPerformsAES128GCMDecryption() {
+ val expectedResult = Bytes.fromHexString("0x19F23925525AF4C2697C1BED166EEB37B5381C10E508A27BCAA02CE661E62A2B")
+
+ val encryptedData = Bytes.fromHexString("0x000000207FC4FDB0E50ACBDA9CD993CFD3A3752104935B91F61B2AF2602C2" +
+ "DC4EFD97AFB943DAB6B1F5A0B13E83C41964F818AB8A51D6D30550BAE8B33A952AA1B6818AB88B66DBD60F5E016FA546808D983B70D")
+ val key = Bytes.fromHexString("0xA924872EAE2DA2C0057ED6DEBD8CAAB8")
+
+ val result = AES128GCM.decrypt(encryptedData, key, Bytes.EMPTY)
+
+ assert(result == expectedResult)
+ }
+}
diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/encrypt/SessionKeyGeneratorTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/encrypt/SessionKeyGeneratorTest.kt
new file mode 100644
index 0000000..f517ed1
--- /dev/null
+++ b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/encrypt/SessionKeyGeneratorTest.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.v5.encrypt
+
+import org.apache.tuweni.bytes.Bytes
+import org.junit.jupiter.api.Test
+
+class SessionKeyGeneratorTest {
+
+ @Test
+ fun generateCreatesSessionKey() {
+ val expectedAuthRespKey = Bytes.fromHexString("0xDC999F3F7EF11907F6762497476117C9")
+ val expectedInitiatorKey = Bytes.fromHexString("0xBBBE757DCE9687BBE5E90CBF9C776163")
+ val expectedRecipientKey = Bytes.fromHexString("0xE83FC3ED3B32DEE7D81D706FECA6174F")
+
+ val srcNodeId = Bytes.fromHexString("0x9CE70B8F317791EB4E775FF9314B9B7B2CD01D90FF5D0E1979B2EBEB92DCB48D")
+ val destNodeId = Bytes.fromHexString("0x0B1E82724DB4D17089EF64A441A2C367683EAC448E6AB7F6F8B3094D2B1B2229")
+ val secret = Bytes.fromHexString("0xAB285AD41C712A917DAC83DE8AAD963285067ED84BAC37052A32BB74DCC75AA5")
+ val idNonce = Bytes.fromHexString("0x630222D6CD1253BF40CB800F230759F117EC1890CD76792135BBC4D7AAD0B4C1")
+
+ val result = SessionKeyGenerator.generate(srcNodeId, destNodeId, secret, idNonce)
+
+ assert(result.authRespKey == expectedAuthRespKey)
+ assert(result.initiatorKey == expectedInitiatorKey)
+ assert(result.recipientKey == expectedRecipientKey)
+ }
+}
diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/internal/DefaultAuthenticationProviderTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/internal/DefaultAuthenticationProviderTest.kt
new file mode 100644
index 0000000..cebe167
--- /dev/null
+++ b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/internal/DefaultAuthenticationProviderTest.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.v5.internal
+
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.crypto.Hash
+import org.apache.tuweni.crypto.SECP256K1
+import org.apache.tuweni.devp2p.EthereumNodeRecord
+import org.apache.tuweni.devp2p.v5.misc.HandshakeInitParameters
+import org.apache.tuweni.devp2p.v5.misc.SessionKey
+import org.apache.tuweni.junit.BouncyCastleExtension
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import java.net.InetAddress
+
+@ExtendWith(BouncyCastleExtension::class)
+class DefaultAuthenticationProviderTest {
+
+ private val providerKeyPair: SECP256K1.KeyPair = SECP256K1.KeyPair.random()
+ private val providerEnr: Bytes = EthereumNodeRecord.toRLP(providerKeyPair, ip = InetAddress.getLocalHost())
+ private val authenticationProvider = DefaultAuthenticationProvider(providerKeyPair, providerEnr)
+
+ @Test
+ fun authenticateReturnsValidAuthHeader() {
+ val keyPair = SECP256K1.KeyPair.random()
+ val nonce = Bytes.fromHexString("0x012715E4EFA2464F51BE49BBC40836E5816B3552249F8AC00AD1BBDB559E44E9")
+ val authTag = Bytes.fromHexString("0x39BBC27C8CFA3735DF436AC6")
+ val destEnr = EthereumNodeRecord.toRLP(keyPair, ip = InetAddress.getLocalHost())
+ val params = HandshakeInitParameters(nonce, authTag, destEnr)
+
+ val result = authenticationProvider.authenticate(params)
+
+ assert(result.idNonce == nonce)
+ assert(result.authTag == authTag)
+ assert(result.authScheme == "gcm")
+ assert(result.ephemeralPublicKey != providerKeyPair.publicKey().bytes())
+
+ val destNodeId = Hash.sha2_256(destEnr).toHexString()
+
+ assert(authenticationProvider.findSessionKey(destNodeId) != null)
+ }
+
+ @Test
+ fun finalizeHandshakePersistsCreatedSessionKeys() {
+ val keyPair = SECP256K1.KeyPair.random()
+ val nonce = Bytes.fromHexString("0x012715E4EFA2464F51BE49BBC40836E5816B3552249F8AC00AD1BBDB559E44E9")
+ val authTag = Bytes.fromHexString("0x39BBC27C8CFA3735DF436AC6")
+ val destEnr = EthereumNodeRecord.toRLP(keyPair, ip = InetAddress.getLocalHost())
+ val params = HandshakeInitParameters(nonce, authTag, providerEnr)
+ val destNodeId = Hash.sha2_256(destEnr)
+
+ val clientAuthProvider = DefaultAuthenticationProvider(keyPair, destEnr)
+
+ val authHeader = clientAuthProvider.authenticate(params)
+
+ authenticationProvider.finalizeHandshake(destNodeId, authHeader)
+
+ assert(authenticationProvider.findSessionKey(destNodeId.toHexString()) != null)
+ }
+
+ @Test
+ fun findSessionKeyRetrievesSessionKeyIfExists() {
+ val result = authenticationProvider.findSessionKey(Bytes.random(32).toHexString())
+
+ assert(result == null)
+ }
+
+ @Test
+ fun setSessionKeyPersistsSessionKeyIfExists() {
+ val nodeId = Bytes.random(32).toHexString()
+ val bytes = Bytes.random(32)
+ val sessionKey = SessionKey(bytes, bytes, bytes)
+
+ authenticationProvider.setSessionKey(nodeId, sessionKey)
+
+ val result = authenticationProvider.findSessionKey(nodeId)
+
+ assert(result != null)
+ }
+}
diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/internal/DefaultPacketCodecTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/internal/DefaultPacketCodecTest.kt
new file mode 100644
index 0000000..85a08dd
--- /dev/null
+++ b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/internal/DefaultPacketCodecTest.kt
@@ -0,0 +1,147 @@
+/*
+ * 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.v5.internal
+
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.crypto.Hash
+import org.apache.tuweni.crypto.SECP256K1
+import org.apache.tuweni.devp2p.EthereumNodeRecord
+import org.apache.tuweni.devp2p.v5.AuthenticationProvider
+import org.apache.tuweni.devp2p.v5.PacketCodec
+import org.apache.tuweni.devp2p.v5.encrypt.AES128GCM
+import org.apache.tuweni.devp2p.v5.misc.SessionKey
+import org.apache.tuweni.devp2p.v5.packet.FindNodeMessage
+import org.apache.tuweni.devp2p.v5.packet.RandomMessage
+import org.apache.tuweni.devp2p.v5.packet.UdpMessage
+import org.apache.tuweni.devp2p.v5.packet.WhoAreYouMessage
+import org.apache.tuweni.junit.BouncyCastleExtension
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import org.junit.jupiter.api.extension.ExtendWith
+import java.net.InetAddress
+
+@ExtendWith(BouncyCastleExtension::class)
+class DefaultPacketCodecTest {
+
+ private val keyPair: SECP256K1.KeyPair = SECP256K1.KeyPair.random()
+ private val enr: Bytes = EthereumNodeRecord.toRLP(keyPair, ip = InetAddress.getLocalHost())
+ private val nodeId: Bytes = Hash.sha2_256(enr)
+ private val authenticationProvider: AuthenticationProvider = DefaultAuthenticationProvider(keyPair, enr)
+
+ private val codec: PacketCodec = DefaultPacketCodec(keyPair, enr, nodeId, authenticationProvider)
+
+ private val destNodeId: Bytes = Bytes.random(32)
+
+ @Test
+ fun encodePerformsValidEncodingOfRandomMessasge() {
+ val message = RandomMessage()
+
+ val encodedResult = codec.encode(message, destNodeId)
+
+ val encodedContent = encodedResult.slice(45)
+ val result = RandomMessage.create(encodedContent)
+
+ assert(result.data == message.data)
+ }
+
+ @Test
+ fun encodePerformsValidEncodingOfWhoAreYouMessage() {
+ val message = WhoAreYouMessage()
+
+ val encodedResult = codec.encode(message, destNodeId)
+
+ val encodedContent = encodedResult.slice(32)
+ val result = WhoAreYouMessage.create(encodedContent)
+
+ assert(result.idNonce == message.idNonce)
+ assert(result.enrSeq == message.enrSeq)
+ assert(result.authTag == message.authTag)
+ }
+
+ @Test
+ fun encodePerformsValidEncodingOfMessagesWithTypeIncluded() {
+ val message = FindNodeMessage()
+
+ val key = Bytes.random(16)
+ val sessionKey = SessionKey(key, key, key)
+
+ authenticationProvider.setSessionKey(destNodeId.toHexString(), sessionKey)
+
+ val encodedResult = codec.encode(message, destNodeId)
+
+ val tag = encodedResult.slice(0, UdpMessage.TAG_LENGTH)
+ val encryptedContent = encodedResult.slice(45)
+ val content = AES128GCM.decrypt(encryptedContent, sessionKey.initiatorKey, tag).slice(1)
+ val result = FindNodeMessage.create(content)
+
+ assert(result.requestId == message.requestId)
+ assert(result.distance == message.distance)
+ }
+
+ @Test
+ fun encodeFailsIfSessionKeyIsNotExists() {
+ val message = FindNodeMessage()
+
+ assertThrows<IllegalArgumentException> {
+ codec.encode(message, destNodeId)
+ }
+ }
+
+ @Test
+ fun decodePerformsValidDecodingOfRandomMessasge() {
+ val message = RandomMessage()
+
+ val encodedResult = codec.encode(message, destNodeId)
+
+ val result = codec.decode(encodedResult).message as? RandomMessage
+
+ assert(null != result)
+ assert(result!!.data == message.data)
+ }
+
+ @Test
+ fun decodePerformsValidDecodingOfWhoAreYouMessage() {
+ val message = WhoAreYouMessage()
+
+ val encodedResult = codec.encode(message, destNodeId)
+
+ val result = codec.decode(encodedResult).message as? WhoAreYouMessage
+
+ assert(null != result)
+ assert(result!!.idNonce == message.idNonce)
+ assert(result.enrSeq == message.enrSeq)
+ assert(result.authTag == message.authTag)
+ }
+
+ @Test
+ fun decodePerformsValidDecodingOfMessagesWithTypeIncluded() {
+ val message = FindNodeMessage()
+
+ val key = Bytes.random(16)
+ val sessionKey = SessionKey(key, key, key)
+
+ authenticationProvider.setSessionKey(destNodeId.toHexString(), sessionKey)
+ authenticationProvider.setSessionKey(nodeId.toHexString(), sessionKey)
+
+ val encodedResult = codec.encode(message, nodeId)
+
+ val result = codec.decode(encodedResult).message as? FindNodeMessage
+
+ assert(result!!.requestId == message.requestId)
+ assert(result.distance == message.distance)
+ }
+}
diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/internal/DefaultUdpConnectorTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/internal/DefaultUdpConnectorTest.kt
new file mode 100644
index 0000000..a345208
--- /dev/null
+++ b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/internal/DefaultUdpConnectorTest.kt
@@ -0,0 +1,102 @@
+/*
+ * 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.v5.internal
+
+import kotlinx.coroutines.runBlocking
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.crypto.SECP256K1
+import org.apache.tuweni.devp2p.EthereumNodeRecord
+import org.apache.tuweni.devp2p.v5.UdpConnector
+import org.apache.tuweni.devp2p.v5.packet.RandomMessage
+import org.apache.tuweni.devp2p.v5.packet.UdpMessage
+import org.apache.tuweni.junit.BouncyCastleExtension
+import org.apache.tuweni.net.coroutines.CoroutineDatagramChannel
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import java.net.InetAddress
+import java.net.InetSocketAddress
+import java.nio.ByteBuffer
+
+@ExtendWith(BouncyCastleExtension::class)
+class DefaultUdpConnectorTest {
+
+ private val keyPair: SECP256K1.KeyPair = SECP256K1.KeyPair.random()
+ private val nodeId: Bytes = Bytes.fromHexString("0x98EB6D611291FA21F6169BFF382B9369C33D997FE4DC93410987E27796360640")
+ private val address: InetSocketAddress = InetSocketAddress(9090)
+ private val selfEnr: Bytes = EthereumNodeRecord.toRLP(keyPair, ip = address.address)
+
+ private val data: Bytes = UdpMessage.randomData()
+ private val message: RandomMessage = RandomMessage(data)
+
+ private var connector: UdpConnector = DefaultUdpConnector(address, keyPair, selfEnr, nodeId)
+
+ @BeforeEach
+ fun setUp() {
+ connector = DefaultUdpConnector(address, keyPair, selfEnr, nodeId)
+ }
+
+ @AfterEach
+ fun tearDown() {
+ if (connector.started()) {
+ connector.terminate()
+ }
+ }
+
+ @Test
+ fun startOpensChannelForMessages() {
+ connector.start()
+
+ assert(connector.available())
+ }
+
+ @Test
+ fun terminateShutdownsConnector() {
+ connector.start()
+
+ assert(connector.available())
+
+ connector.terminate()
+
+ assert(!connector.available())
+ }
+
+ @Test
+ fun sendSendsValidDatagram() {
+ connector.start()
+
+ val destNodeId = Bytes.random(32)
+
+ val receiverAddress = InetSocketAddress(InetAddress.getLocalHost(), 9091)
+ val socketChannel = CoroutineDatagramChannel.open()
+ socketChannel.bind(receiverAddress)
+
+ runBlocking {
+ connector.send(receiverAddress, message, destNodeId)
+ val buffer = ByteBuffer.allocate(UdpMessage.MAX_UDP_MESSAGE_SIZE)
+ socketChannel.receive(buffer) as InetSocketAddress
+ buffer.flip()
+
+ val messageContent = Bytes.wrapByteBuffer(buffer).slice(45)
+ val message = RandomMessage.create(messageContent)
+
+ assert(message.data == data)
+ }
+ socketChannel.close()
+ }
+}
diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/FindNodeMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/FindNodeMessageTest.kt
new file mode 100644
index 0000000..22b323c
--- /dev/null
+++ b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/FindNodeMessageTest.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.v5.packet
+
+import org.apache.tuweni.bytes.Bytes
+import org.junit.jupiter.api.Test
+
+class FindNodeMessageTest {
+
+ @Test
+ fun encodeCreatesValidBytesSequence() {
+ val expectedEncodingResult = "0xCA88C6E32C5E89CAA75480"
+
+ val requestId = Bytes.fromHexString("0xC6E32C5E89CAA754")
+ val message = FindNodeMessage(requestId)
+
+ val encodingResult = message.encode()
+ assert(encodingResult.toHexString() == expectedEncodingResult)
+
+ val decodingResult = FindNodeMessage.create(encodingResult)
+
+ assert(decodingResult.requestId == requestId)
+ assert(decodingResult.distance == 0L)
+ }
+
+ @Test
+ fun getMessageTypeHasValidIndex() {
+ val message = FindNodeMessage()
+
+ assert(3 == message.getMessageType().toInt())
+ }
+}
diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/NodesMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/NodesMessageTest.kt
new file mode 100644
index 0000000..d65696d
--- /dev/null
+++ b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/NodesMessageTest.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.v5.packet
+
+import org.apache.tuweni.bytes.Bytes
+import org.apache.tuweni.crypto.SECP256K1
+import org.apache.tuweni.devp2p.EthereumNodeRecord
+import org.apache.tuweni.junit.BouncyCastleExtension
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import java.net.InetAddress
+
+@ExtendWith(BouncyCastleExtension::class)
+class NodesMessageTest {
+
+ @Test
+ fun encodeCreatesValidBytesSequence() {
+ val requestId = Bytes.fromHexString("0xC6E32C5E89CAA754")
+ val total = 10
+ val nodeRecords = listOf(
+ EthereumNodeRecord.toRLP(SECP256K1.KeyPair.random(), ip = InetAddress.getLocalHost(), udp = 9090),
+ EthereumNodeRecord.toRLP(SECP256K1.KeyPair.random(), ip = InetAddress.getLocalHost(), udp = 9091),
+ EthereumNodeRecord.toRLP(SECP256K1.KeyPair.random(), ip = InetAddress.getLocalHost(), udp = 9092)
+ )
+ val message = NodesMessage(requestId, total, nodeRecords)
+
+ val encodingResult = message.encode()
+
+ val decodingResult = NodesMessage.create(encodingResult)
+
+ assert(decodingResult.requestId == requestId)
+ assert(decodingResult.total == 10)
+ assert(EthereumNodeRecord.fromRLP(decodingResult.nodeRecords[0]).udp() == 9090)
+ assert(EthereumNodeRecord.fromRLP(decodingResult.nodeRecords[1]).udp() == 9091)
+ assert(EthereumNodeRecord.fromRLP(decodingResult.nodeRecords[2]).udp() == 9092)
+ }
+
+ @Test
+ fun getMessageTypeHasValidIndex() {
+ val message = NodesMessage(UdpMessage.requestId(), 0, emptyList())
+
+ assert(4 == message.getMessageType().toInt())
+ }
+}
diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/PingMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/PingMessageTest.kt
new file mode 100644
index 0000000..554437d
--- /dev/null
+++ b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/PingMessageTest.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.v5.packet
+
+import org.apache.tuweni.bytes.Bytes
+import org.junit.jupiter.api.Test
+
+class PingMessageTest {
+
+ @Test
+ fun encodeCreatesValidBytesSequence() {
+ val requestId = Bytes.fromHexString("0xC6E32C5E89CAA754")
+ val message = PingMessage(requestId)
+
+ val encodingResult = message.encode()
+
+ val decodingResult = PingMessage.create(encodingResult)
+
+ assert(decodingResult.requestId == requestId)
+ assert(decodingResult.enrSeq == message.enrSeq)
+ }
+
+ @Test
+ fun getMessageTypeHasValidIndex() {
+ val message = PingMessage()
+
+ assert(1 == message.getMessageType().toInt())
+ }
+}
diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/PongMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/PongMessageTest.kt
new file mode 100644
index 0000000..e0847c4
--- /dev/null
+++ b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/PongMessageTest.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.v5.packet
+
+import org.apache.tuweni.bytes.Bytes
+import org.junit.jupiter.api.Test
+import java.net.InetAddress
+
+class PongMessageTest {
+
+ @Test
+ fun encodeCreatesValidBytesSequence() {
+ val requestId = Bytes.fromHexString("0xC6E32C5E89CAA754")
+ val message = PongMessage(requestId, 0, InetAddress.getLocalHost(), 9090)
+
+ val encodingResult = message.encode()
+
+ val decodingResult = PongMessage.create(encodingResult)
+
+ assert(decodingResult.requestId == requestId)
+ assert(decodingResult.enrSeq == message.enrSeq)
+ assert(decodingResult.recipientIp == message.recipientIp)
+ assert(decodingResult.recipientPort == message.recipientPort)
+ }
+
+ @Test
+ fun getMessageTypeHasValidIndex() {
+ val message = PongMessage(recipientIp = InetAddress.getLocalHost(), recipientPort = 9090)
+
+ assert(2 == message.getMessageType().toInt())
+ }
+}
diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/RandomMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/RandomMessageTest.kt
new file mode 100644
index 0000000..2060816
--- /dev/null
+++ b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/RandomMessageTest.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.v5.packet
+
+import org.apache.tuweni.bytes.Bytes
+import org.junit.jupiter.api.Test
+
+class RandomMessageTest {
+
+ @Test
+ fun encodeCreatesValidBytesSequence() {
+ val expectedEncodingResult =
+ "0xB53CCF732982B8E950836D1E02898C8B38CFDBFDF86BC65C8826506B454E14618EA73612A0F5582C130FF666"
+
+ val data = Bytes.fromHexString(expectedEncodingResult)
+ val message = RandomMessage(data)
+
+ val encodingResult = message.encode()
+ assert(encodingResult.toHexString() == expectedEncodingResult)
+
+ val decodingResult = RandomMessage.create(encodingResult)
+
+ assert(decodingResult.data == data)
+ }
+}
diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/RegConfirmationMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/RegConfirmationMessageTest.kt
new file mode 100644
index 0000000..65f61a6
--- /dev/null
+++ b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/RegConfirmationMessageTest.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.v5.packet
+
+import org.apache.tuweni.bytes.Bytes
+import org.junit.jupiter.api.Test
+
+class RegConfirmationMessageTest {
+
+ @Test
+ fun encodeCreatesValidBytesSequence() {
+ val requestId = Bytes.fromHexString("0xC6E32C5E89CAA754")
+ val message = RegConfirmationMessage(requestId, false)
+
+ val encodingResult = message.encode()
+
+ val decodingResult = RegConfirmationMessage.create(encodingResult)
+
+ assert(decodingResult.requestId == requestId)
+ assert(decodingResult.registered == message.registered)
+ }
+
+ @Test
+ fun getMessageTypeHasValidIndex() {
+ val message = RegConfirmationMessage()
+
+ assert(8 == message.getMessageType().toInt())
+ }
+}
diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/RegTopicMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/RegTopicMessageTest.kt
new file mode 100644
index 0000000..c33b43b
--- /dev/null
+++ b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/RegTopicMessageTest.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.v5.packet
+
+import org.apache.tuweni.bytes.Bytes
+import org.junit.jupiter.api.Test
+
+class RegTopicMessageTest {
+
+ @Test
+ fun encodeCreatesValidBytesSequence() {
+ val requestId = Bytes.fromHexString("0xC6E32C5E89CAA754")
+ val message = RegTopicMessage(requestId, Bytes.random(32), Bytes.random(32))
+
+ val encodingResult = message.encode()
+
+ val decodingResult = RegTopicMessage.create(encodingResult)
+
+ assert(decodingResult.requestId == requestId)
+ assert(decodingResult.ticket == message.ticket)
+ assert(decodingResult.nodeRecord == message.nodeRecord)
+ }
+
+ @Test
+ fun getMessageTypeHasValidIndex() {
+ val message = RegTopicMessage(ticket = Bytes.random(32), nodeRecord = Bytes.random(32))
+
+ assert(7 == message.getMessageType().toInt())
+ }
+}
diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/ReqTicketMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/ReqTicketMessageTest.kt
new file mode 100644
index 0000000..a43a328
--- /dev/null
+++ b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/ReqTicketMessageTest.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.v5.packet
+
+import org.apache.tuweni.bytes.Bytes
+import org.junit.jupiter.api.Test
+
+class ReqTicketMessageTest {
+
+ @Test
+ fun encodeCreatesValidBytesSequence() {
+ val requestId = Bytes.fromHexString("0xC6E32C5E89CAA754")
+ val message = ReqTicketMessage(requestId, Bytes.random(32))
+
+ val encodingResult = message.encode()
+
+ val decodingResult = ReqTicketMessage.create(encodingResult)
+
+ assert(decodingResult.requestId == requestId)
+ assert(decodingResult.topic == message.topic)
+ }
+
+ @Test
+ fun getMessageTypeHasValidIndex() {
+ val message = ReqTicketMessage(topic = Bytes.random(32))
+
+ assert(5 == message.getMessageType().toInt())
+ }
+}
diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/TicketMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/TicketMessageTest.kt
new file mode 100644
index 0000000..f754489
--- /dev/null
+++ b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/TicketMessageTest.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.v5.packet
+
+import org.apache.tuweni.bytes.Bytes
+import org.junit.jupiter.api.Test
+
+class TicketMessageTest {
+
+ @Test
+ fun encodeCreatesValidBytesSequence() {
+ val requestId = Bytes.fromHexString("0xC6E32C5E89CAA754")
+ val message = TicketMessage(requestId, Bytes.random(32), 1000)
+
+ val encodingResult = message.encode()
+
+ val decodingResult = TicketMessage.create(encodingResult)
+
+ assert(decodingResult.requestId == requestId)
+ assert(decodingResult.ticket == message.ticket)
+ assert(decodingResult.waitTime == message.waitTime)
+ }
+
+ @Test
+ fun getMessageTypeHasValidIndex() {
+ val message = TicketMessage(ticket = Bytes.random(32), waitTime = 1000)
+
+ assert(6 == message.getMessageType().toInt())
+ }
+}
diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/TopicQueryMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/TopicQueryMessageTest.kt
new file mode 100644
index 0000000..36d66a8
--- /dev/null
+++ b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/TopicQueryMessageTest.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.v5.packet
+
+import org.apache.tuweni.bytes.Bytes
+import org.junit.jupiter.api.Test
+
+class TopicQueryMessageTest {
+
+ @Test
+ fun encodeCreatesValidBytesSequence() {
+ val requestId = Bytes.fromHexString("0xC6E32C5E89CAA754")
+ val message = TopicQueryMessage(requestId, Bytes.random(32))
+
+ val encodingResult = message.encode()
+
+ val decodingResult = TopicQueryMessage.create(encodingResult)
+
+ assert(decodingResult.requestId == requestId)
+ assert(decodingResult.topic == message.topic)
+ }
+
+ @Test
+ fun getMessageTypeHasValidIndex() {
+ val message = TopicQueryMessage(topic = Bytes.random(32))
+
+ assert(9 == message.getMessageType().toInt())
+ }
+}
diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/UdpMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/UdpMessageTest.kt
new file mode 100644
index 0000000..f6881cb
--- /dev/null
+++ b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/UdpMessageTest.kt
@@ -0,0 +1,96 @@
+/*
+ * 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.v5.packet
+
+import org.apache.tuweni.bytes.Bytes
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+
+class UdpMessageTest {
+
+ @Test
+ fun magicCreatesSha256OfDestNodeIdAndConstantString() {
+ val destId = Bytes.fromHexString("0xA5CFE10E0EFC543CBE023560B2900E2243D798FAFD0EA46267DDD20D283CE13C")
+ val expected = Bytes.fromHexString("0x98EB6D611291FA21F6169BFF382B9369C33D997FE4DC93410987E27796360640")
+
+ val result = UdpMessage.magic(destId)
+
+ assert(expected == result)
+ }
+
+ @Test
+ fun tagHashesSourceAndDestNodeIdCorrectly() {
+ val srcId = Bytes.fromHexString("0x98EB6D611291FA21F6169BFF382B9369C33D997FE4DC93410987E27796360640")
+ val destId = Bytes.fromHexString("0xA5CFE10E0EFC543CBE023560B2900E2243D798FAFD0EA46267DDD20D283CE13C")
+ val expected = Bytes.fromHexString("0xB7A0D7CA8BD37611315DA0882FF479DE14B442FD30AE0EFBE6FC6344D55DC632")
+
+ val result = UdpMessage.tag(srcId, destId)
+
+ assert(expected == result)
+ }
+
+ @Test
+ fun getSourceFromTagFetchesSrcNodeId() {
+ val srcId = Bytes.fromHexString("0x98EB6D611291FA21F6169BFF382B9369C33D997FE4DC93410987E27796360640")
+ val destId = Bytes.fromHexString("0xA5CFE10E0EFC543CBE023560B2900E2243D798FAFD0EA46267DDD20D283CE13C")
+ val tag = UdpMessage.tag(srcId, destId)
+
+ val result = UdpMessage.getSourceFromTag(tag, destId)
+
+ assert(srcId == result)
+ }
+
+ @Test
+ fun authTagGivesRandom12Bytes() {
+ val firstResult = UdpMessage.authTag()
+
+ assert(UdpMessage.AUTH_TAG_LENGTH == firstResult.size())
+
+ val secondResult = UdpMessage.authTag()
+
+ assert(secondResult != firstResult)
+ }
+
+ @Test
+ fun randomDataGivesRandom44Bytes() {
+ val firstResult = UdpMessage.randomData()
+
+ assert(UdpMessage.RANDOM_DATA_LENGTH == firstResult.size())
+
+ val secondResult = UdpMessage.randomData()
+
+ assert(secondResult != firstResult)
+ }
+
+ @Test
+ fun idNonceGivesRandom32Bytes() {
+ val firstResult = UdpMessage.idNonce()
+
+ assert(UdpMessage.ID_NONCE_LENGTH == firstResult.size())
+
+ val secondResult = UdpMessage.idNonce()
+
+ assert(secondResult != firstResult)
+ }
+
+ @Test
+ fun getMessageTypeThrowsExceptionByDefault() {
+ assertThrows<UnsupportedOperationException> {
+ RandomMessage().getMessageType()
+ }
+ }
+}
diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/WhoAreYouMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/WhoAreYouMessageTest.kt
new file mode 100644
index 0000000..12ad2d2
--- /dev/null
+++ b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/packet/WhoAreYouMessageTest.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.v5.packet
+
+import org.apache.tuweni.bytes.Bytes
+import org.junit.jupiter.api.Test
+
+class WhoAreYouMessageTest {
+
+ @Test
+ fun encodeCreatesValidBytesSequence() {
+ val expectedEncodingResult =
+ "0xEF8C05D038D54B1ACB9A2A83C480A0C3B548CA063DA57BC9DE93340360AF32815FC8D0B2F053B3CB7918ABBB291A5180"
+
+ val authTag = Bytes.fromHexString("0x05D038D54B1ACB9A2A83C480")
+ val nonce = Bytes.fromHexString("0xC3B548CA063DA57BC9DE93340360AF32815FC8D0B2F053B3CB7918ABBB291A51")
+ val message = WhoAreYouMessage(authTag, nonce)
+
+ val encodingResult = message.encode()
+ assert(encodingResult.toHexString() == expectedEncodingResult)
+
+ val decodingResult = WhoAreYouMessage.create(encodingResult)
+
+ assert(decodingResult.authTag == authTag)
+ assert(decodingResult.idNonce == nonce)
+ assert(decodingResult.enrSeq == 0L)
+ }
+}
diff --git a/rlp/src/main/java/org/apache/tuweni/rlp/BytesRLPReader.java b/rlp/src/main/java/org/apache/tuweni/rlp/BytesRLPReader.java
index d96b012..0d43c43 100644
--- a/rlp/src/main/java/org/apache/tuweni/rlp/BytesRLPReader.java
+++ b/rlp/src/main/java/org/apache/tuweni/rlp/BytesRLPReader.java
@@ -196,6 +196,11 @@
return (content.size() - index) == 0;
}
+ @Override
+ public int position() {
+ return index;
+ }
+
private Bytes readList(boolean lenient) {
int remaining = content.size() - index;
if (remaining == 0) {
diff --git a/rlp/src/main/java/org/apache/tuweni/rlp/RLPReader.java b/rlp/src/main/java/org/apache/tuweni/rlp/RLPReader.java
index f9cc643..05e279f 100644
--- a/rlp/src/main/java/org/apache/tuweni/rlp/RLPReader.java
+++ b/rlp/src/main/java/org/apache/tuweni/rlp/RLPReader.java
@@ -399,4 +399,11 @@
* @return {@code true} if all values have been read.
*/
boolean isComplete();
+
+ /**
+ * Returns reader's index
+ *
+ * @return current reader position
+ */
+ int position();
}