]
+ * This file is part of the ethereumJ library.
+ *
+ * The ethereumJ library is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * The ethereumJ library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with the ethereumJ library. If not, see .
+ */
+// $Id: Digest.java 232 2010-06-17 14:19:24Z tp $
+
+package io.horizontalsystems.ethereumkit.light.crypto.digest;
+
+/**
+ * This interface documents the API for a hash function. This
+ * interface somewhat mimics the standard {@code
+ * java.security.MessageDigest} class. We do not extend that class in
+ * order to provide compatibility with reduced Java implementations such
+ * as J2ME. Implementing a {@code java.security.Provider} compatible
+ * with Sun's JCA ought to be easy.
+ *
+ * A {@code Digest} object maintains a running state for a hash
+ * function computation. Data is inserted with {@code update()} calls;
+ * the result is obtained from a {@code digest()} method (where some
+ * final data can be inserted as well). When a digest output has been
+ * produced, the objet is automatically resetted, and can be used
+ * immediately for another digest operation. The state of a computation
+ * can be cloned with the {@link #copy} method; this can be used to get
+ * a partial hash result without interrupting the complete
+ * computation.
+ *
+ * {@code Digest} objects are stateful and hence not thread-safe;
+ * however, distinct {@code Digest} objects can be accessed concurrently
+ * without any problem.
+ *
+ *
+ * ==========================(LICENSE BEGIN)============================
+ *
+ * Copyright (c) 2007-2010 Projet RNRT SAPHIR
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * ===========================(LICENSE END)=============================
+ *
+ *
+ * @version $Revision: 232 $
+ * @author Thomas Pornin <thomas.pornin@cryptolog.com>
+ */
+
+public interface Digest{
+
+ /**
+ * Insert one more input data byte.
+ *
+ * @param in the input byte
+ */
+ void update(byte in);
+
+ /**
+ * Insert some more bytes.
+ *
+ * @param inbuf the data bytes
+ */
+ void update(byte[] inbuf);
+
+ /**
+ * Insert some more bytes.
+ *
+ * @param inbuf the data buffer
+ * @param off the data offset in {@code inbuf}
+ * @param len the data length (in bytes)
+ */
+ void update(byte[] inbuf, int off, int len);
+
+ /**
+ * Finalize the current hash computation and return the hash value
+ * in a newly-allocated array. The object is resetted.
+ *
+ * @return the hash output
+ */
+ byte[] digest();
+
+ /**
+ * Input some bytes, then finalize the current hash computation
+ * and return the hash value in a newly-allocated array. The object
+ * is resetted.
+ *
+ * @param inbuf the input data
+ * @return the hash output
+ */
+ byte[] digest(byte[] inbuf);
+
+ /**
+ * Finalize the current hash computation and store the hash value
+ * in the provided output buffer. The {@code len} parameter
+ * contains the maximum number of bytes that should be written;
+ * no more bytes than the natural hash function output length will
+ * be produced. If {@code len} is smaller than the natural
+ * hash output length, the hash output is truncated to its first
+ * {@code len} bytes. The object is resetted.
+ *
+ * @param outbuf the output buffer
+ * @param off the output offset within {@code outbuf}
+ * @param len the requested hash output length (in bytes)
+ * @return the number of bytes actually written in {@code outbuf}
+ */
+ int digest(byte[] outbuf, int off, int len);
+
+ /**
+ * Get the natural hash function output length (in bytes).
+ *
+ * @return the digest output length (in bytes)
+ */
+ int getDigestLength();
+
+ /**
+ * Reset the object: this makes it suitable for a new hash
+ * computation. The current computation, if any, is discarded.
+ */
+ void reset();
+
+ /**
+ * Clone the current state. The returned object evolves independantly
+ * of this object.
+ *
+ * @return the clone
+ */
+ Digest copy();
+
+ /**
+ * Return the "block length" for the hash function. This
+ * value is naturally defined for iterated hash functions
+ * (Merkle-Damgard). It is used in HMAC (that's what the
+ * HMAC specification
+ * names the "{@code B}" parameter).
+ *
+ * If the function is "block-less" then this function may
+ * return {@code -n} where {@code n} is an integer such that the
+ * block length for HMAC ("{@code B}") will be inferred from the
+ * key length, by selecting the smallest multiple of {@code n}
+ * which is no smaller than the key length. For instance, for
+ * the Fugue-xxx hash functions, this function returns -4: the
+ * virtual block length B is the HMAC key length, rounded up to
+ * the next multiple of 4.
+ *
+ * @return the internal block length (in bytes), or {@code -n}
+ */
+ int getBlockLength();
+
+ /**
+ * Get the display name for this function (e.g. {@code "SHA-1"}
+ * for SHA-1).
+ *
+ * @see Object
+ */
+ String toString();
+}
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/crypto/digest/DigestEngine.java b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/crypto/digest/DigestEngine.java
new file mode 100644
index 00000000..694f2f95
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/crypto/digest/DigestEngine.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (c) [2016] [ ]
+ * This file is part of the ethereumJ library.
+ *
+ * The ethereumJ library is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * The ethereumJ library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with the ethereumJ library. If not, see .
+ */
+// $Id: DigestEngine.java 229 2010-06-16 20:22:27Z tp $
+
+package io.horizontalsystems.ethereumkit.light.crypto.digest;
+
+import java.security.MessageDigest;
+
+/**
+ * This class is a template which can be used to implement hash
+ * functions. It takes care of some of the API, and also provides an
+ * internal data buffer whose length is equal to the hash function
+ * internal block length.
+ *
+ * Classes which use this template MUST provide a working {@link
+ * #getBlockLength} method even before initialization (alternatively,
+ * they may define a custom {@link #getInternalBlockLength} which does
+ * not call {@link #getBlockLength}. The {@link #getDigestLength} should
+ * also be operational from the beginning, but it is acceptable that it
+ * returns 0 while the {@link #doInit} method has not been called
+ * yet.
+ *
+ *
+ * ==========================(LICENSE BEGIN)============================
+ *
+ * Copyright (c) 2007-2010 Projet RNRT SAPHIR
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * ===========================(LICENSE END)=============================
+ *
+ *
+ * @version $Revision: 229 $
+ * @author Thomas Pornin <thomas.pornin@cryptolog.com>
+ */
+
+public abstract class DigestEngine extends MessageDigest implements Digest {
+
+ /**
+ * Reset the hash algorithm state.
+ */
+ protected abstract void engineReset();
+
+ /**
+ * Process one block of data.
+ *
+ * @param data the data block
+ */
+ protected abstract void processBlock(byte[] data);
+
+ /**
+ * Perform the final padding and store the result in the
+ * provided buffer. This method shall call {@link #flush}
+ * and then {@link #update} with the appropriate padding
+ * data in order to get the full input data.
+ *
+ * @param buf the output buffer
+ * @param off the output offset
+ */
+ protected abstract void doPadding(byte[] buf, int off);
+
+ /**
+ * This function is called at object creation time; the
+ * implementation should use it to perform initialization tasks.
+ * After this method is called, the implementation should be ready
+ * to process data or meaningfully honour calls such as
+ * {@link #engineGetDigestLength}
+ */
+ protected abstract void doInit();
+
+ private int digestLen, blockLen, inputLen;
+ private byte[] inputBuf, outputBuf;
+ private long blockCount;
+
+ /**
+ * Instantiate the engine.
+ */
+ public DigestEngine(String alg)
+ {
+ super(alg);
+ doInit();
+ digestLen = engineGetDigestLength();
+ blockLen = getInternalBlockLength();
+ inputBuf = new byte[blockLen];
+ outputBuf = new byte[digestLen];
+ inputLen = 0;
+ blockCount = 0;
+ }
+
+ private void adjustDigestLen()
+ {
+ if (digestLen == 0) {
+ digestLen = engineGetDigestLength();
+ outputBuf = new byte[digestLen];
+ }
+ }
+
+ public byte[] digest()
+ {
+ adjustDigestLen();
+ byte[] result = new byte[digestLen];
+ digest(result, 0, digestLen);
+ return result;
+ }
+
+ public byte[] digest(byte[] input)
+ {
+ update(input, 0, input.length);
+ return digest();
+ }
+
+ public int digest(byte[] buf, int offset, int len)
+ {
+ adjustDigestLen();
+ if (len >= digestLen) {
+ doPadding(buf, offset);
+ reset();
+ return digestLen;
+ } else {
+ doPadding(outputBuf, 0);
+ System.arraycopy(outputBuf, 0, buf, offset, len);
+ reset();
+ return len;
+ }
+ }
+
+ public void reset()
+ {
+ engineReset();
+ inputLen = 0;
+ blockCount = 0;
+ }
+
+ public void update(byte input)
+ {
+ inputBuf[inputLen ++] = (byte)input;
+ if (inputLen == blockLen) {
+ processBlock(inputBuf);
+ blockCount ++;
+ inputLen = 0;
+ }
+ }
+
+ public void update(byte[] input)
+ {
+ update(input, 0, input.length);
+ }
+
+ public void update(byte[] input, int offset, int len)
+ {
+ while (len > 0) {
+ int copyLen = blockLen - inputLen;
+ if (copyLen > len)
+ copyLen = len;
+ System.arraycopy(input, offset, inputBuf, inputLen,
+ copyLen);
+ offset += copyLen;
+ inputLen += copyLen;
+ len -= copyLen;
+ if (inputLen == blockLen) {
+ processBlock(inputBuf);
+ blockCount ++;
+ inputLen = 0;
+ }
+ }
+ }
+
+ /**
+ * Get the internal block length. This is the length (in
+ * bytes) of the array which will be passed as parameter to
+ * {@link #processBlock}. The default implementation of this
+ * method calls {@link #getBlockLength} and returns the same
+ * value. Overriding this method is useful when the advertised
+ * block length (which is used, for instance, by HMAC) is
+ * suboptimal with regards to internal buffering needs.
+ *
+ * @return the internal block length (in bytes)
+ */
+ protected int getInternalBlockLength()
+ {
+ return getBlockLength();
+ }
+
+ /**
+ * Flush internal buffers, so that less than a block of data
+ * may at most be upheld.
+ *
+ * @return the number of bytes still unprocessed after the flush
+ */
+ protected final int flush()
+ {
+ return inputLen;
+ }
+
+ /**
+ * Get a reference to an internal buffer with the same size
+ * than a block. The contents of that buffer are defined only
+ * immediately after a call to {@link #flush()}: if
+ * {@link #flush()} return the value {@code n}, then the
+ * first {@code n} bytes of the array returned by this method
+ * are the {@code n} bytes of input data which are still
+ * unprocessed. The values of the remaining bytes are
+ * undefined and may be altered at will.
+ *
+ * @return a block-sized internal buffer
+ */
+ protected final byte[] getBlockBuffer()
+ {
+ return inputBuf;
+ }
+
+ /**
+ * Get the "block count": this is the number of times the
+ * {@link #processBlock} method has been invoked for the
+ * current hash operation. That counter is incremented
+ * after the call to {@link #processBlock}.
+ *
+ * @return the block count
+ */
+ protected long getBlockCount()
+ {
+ return blockCount;
+ }
+
+ /**
+ * This function copies the internal buffering state to some
+ * other instance of a class extending {@code DigestEngine}.
+ * It returns a reference to the copy. This method is intended
+ * to be called by the implementation of the {@link #copy}
+ * method.
+ *
+ * @param dest the copy
+ * @return the value {@code dest}
+ */
+ protected Digest copyState(DigestEngine dest)
+ {
+ dest.inputLen = inputLen;
+ dest.blockCount = blockCount;
+ System.arraycopy(inputBuf, 0, dest.inputBuf, 0,
+ inputBuf.length);
+ adjustDigestLen();
+ dest.adjustDigestLen();
+ System.arraycopy(outputBuf, 0, dest.outputBuf, 0,
+ outputBuf.length);
+ return dest;
+ }
+}
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/crypto/digest/Keccak256.java b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/crypto/digest/Keccak256.java
new file mode 100644
index 00000000..f5678a6e
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/crypto/digest/Keccak256.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) [2016] [ ]
+ * This file is part of the ethereumJ library.
+ *
+ * The ethereumJ library is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * The ethereumJ library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with the ethereumJ library. If not, see .
+ */
+// $Id: Keccak256.java 189 2010-05-14 21:21:46Z tp $
+
+package io.horizontalsystems.ethereumkit.light.crypto.digest;
+
+/**
+ * This class implements the Keccak-256 digest algorithm under the
+ * org.ethereum.crypto.cryptohash.Digest API.
+ *
+ *
+ * ==========================(LICENSE BEGIN)============================
+ *
+ * Copyright (c) 2007-2010 Projet RNRT SAPHIR
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * ===========================(LICENSE END)=============================
+ *
+ *
+ * @version $Revision: 189 $
+ * @author Thomas Pornin <thomas.pornin@cryptolog.com>
+ */
+
+public class Keccak256 extends KeccakCore {
+
+ /**
+ * Create the engine.
+ */
+ public Keccak256()
+ {
+ super("eth-keccak-256");
+ }
+
+ public Digest copy()
+ {
+ return copyState(new Keccak256());
+ }
+
+ public int engineGetDigestLength()
+ {
+ return 32;
+ }
+
+ @Override
+ protected byte[] engineDigest() {
+ return null;
+ }
+
+ @Override
+ protected void engineUpdate(byte arg0) {
+ }
+
+ @Override
+ protected void engineUpdate(byte[] arg0, int arg1, int arg2) {
+ }
+}
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/crypto/digest/Keccak512.java b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/crypto/digest/Keccak512.java
new file mode 100644
index 00000000..8af9f741
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/crypto/digest/Keccak512.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) [2016] [ ]
+ * This file is part of the ethereumJ library.
+ *
+ * The ethereumJ library is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * The ethereumJ library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with the ethereumJ library. If not, see .
+ */
+// $Id: Keccak512.java 189 2010-05-14 21:21:46Z tp $
+
+package io.horizontalsystems.ethereumkit.light.crypto.digest;
+
+/**
+ * This class implements the Keccak-256 digest algorithm under the
+ * {@link Digest} API.
+ *
+ *
+ * ==========================(LICENSE BEGIN)============================
+ *
+ * Copyright (c) 2007-2010 Projet RNRT SAPHIR
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * ===========================(LICENSE END)=============================
+ *
+ *
+ * @version $Revision: 189 $
+ * @author Thomas Pornin <thomas.pornin@cryptolog.com>
+ */
+
+public class Keccak512 extends KeccakCore {
+
+ /**
+ * Create the engine.
+ */
+ public Keccak512()
+ {
+ super("eth-keccak-512");
+ }
+
+ /** @see Digest */
+ public Digest copy()
+ {
+ return copyState(new Keccak512());
+ }
+
+ /** @see Digest */
+ public int engineGetDigestLength()
+ {
+ return 64;
+ }
+
+ @Override
+ protected byte[] engineDigest() {
+ return null;
+ }
+
+ @Override
+ protected void engineUpdate(byte input) {
+ }
+
+ @Override
+ protected void engineUpdate(byte[] input, int offset, int len) {
+ }
+}
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/crypto/digest/KeccakCore.java b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/crypto/digest/KeccakCore.java
new file mode 100644
index 00000000..c99f84eb
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/crypto/digest/KeccakCore.java
@@ -0,0 +1,596 @@
+/*
+ * Copyright (c) [2016] [ ]
+ * This file is part of the ethereumJ library.
+ *
+ * The ethereumJ library is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * The ethereumJ library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with the ethereumJ library. If not, see .
+ */
+// $Id: KeccakCore.java 258 2011-07-15 22:16:50Z tp $
+
+package io.horizontalsystems.ethereumkit.light.crypto.digest;
+
+/**
+ * This class implements the core operations for the Keccak digest
+ * algorithm.
+ *
+ *
+ * ==========================(LICENSE BEGIN)============================
+ *
+ * Copyright (c) 2007-2010 Projet RNRT SAPHIR
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * ===========================(LICENSE END)=============================
+ *
+ *
+ * @version $Revision: 258 $
+ * @author Thomas Pornin <thomas.pornin@cryptolog.com>
+ */
+
+abstract class KeccakCore extends DigestEngine{
+
+ KeccakCore(String alg)
+ {
+ super(alg);
+ }
+
+ private long[] A;
+ private byte[] tmpOut;
+
+ private static final long[] RC = {
+ 0x0000000000000001L, 0x0000000000008082L,
+ 0x800000000000808AL, 0x8000000080008000L,
+ 0x000000000000808BL, 0x0000000080000001L,
+ 0x8000000080008081L, 0x8000000000008009L,
+ 0x000000000000008AL, 0x0000000000000088L,
+ 0x0000000080008009L, 0x000000008000000AL,
+ 0x000000008000808BL, 0x800000000000008BL,
+ 0x8000000000008089L, 0x8000000000008003L,
+ 0x8000000000008002L, 0x8000000000000080L,
+ 0x000000000000800AL, 0x800000008000000AL,
+ 0x8000000080008081L, 0x8000000000008080L,
+ 0x0000000080000001L, 0x8000000080008008L
+ };
+
+ /**
+ * Encode the 64-bit word {@code val} into the array
+ * {@code buf} at offset {@code off}, in little-endian
+ * convention (least significant byte first).
+ *
+ * @param val the value to encode
+ * @param buf the destination buffer
+ * @param off the destination offset
+ */
+ private static void encodeLELong(long val, byte[] buf, int off)
+ {
+ buf[off + 0] = (byte)val;
+ buf[off + 1] = (byte)(val >>> 8);
+ buf[off + 2] = (byte)(val >>> 16);
+ buf[off + 3] = (byte)(val >>> 24);
+ buf[off + 4] = (byte)(val >>> 32);
+ buf[off + 5] = (byte)(val >>> 40);
+ buf[off + 6] = (byte)(val >>> 48);
+ buf[off + 7] = (byte)(val >>> 56);
+ }
+
+ /**
+ * Decode a 64-bit little-endian word from the array {@code buf}
+ * at offset {@code off}.
+ *
+ * @param buf the source buffer
+ * @param off the source offset
+ * @return the decoded value
+ */
+ private static long decodeLELong(byte[] buf, int off)
+ {
+ return (buf[off + 0] & 0xFFL)
+ | ((buf[off + 1] & 0xFFL) << 8)
+ | ((buf[off + 2] & 0xFFL) << 16)
+ | ((buf[off + 3] & 0xFFL) << 24)
+ | ((buf[off + 4] & 0xFFL) << 32)
+ | ((buf[off + 5] & 0xFFL) << 40)
+ | ((buf[off + 6] & 0xFFL) << 48)
+ | ((buf[off + 7] & 0xFFL) << 56);
+ }
+
+ protected void engineReset()
+ {
+ doReset();
+ }
+
+ protected void processBlock(byte[] data)
+ {
+ /* Input block */
+ for (int i = 0; i < data.length; i += 8)
+ A[i >>> 3] ^= decodeLELong(data, i);
+
+ long t0, t1, t2, t3, t4;
+ long tt0, tt1, tt2, tt3, tt4;
+ long t, kt;
+ long c0, c1, c2, c3, c4, bnn;
+
+ /*
+ * Unrolling four rounds kills performance big time
+ * on Intel x86 Core2, in both 32-bit and 64-bit modes
+ * (less than 1 MB/s instead of 55 MB/s on x86-64).
+ * Unrolling two rounds appears to be fine.
+ */
+ for (int j = 0; j < 24; j += 2) {
+
+ tt0 = A[ 1] ^ A[ 6];
+ tt1 = A[11] ^ A[16];
+ tt0 ^= A[21] ^ tt1;
+ tt0 = (tt0 << 1) | (tt0 >>> 63);
+ tt2 = A[ 4] ^ A[ 9];
+ tt3 = A[14] ^ A[19];
+ tt0 ^= A[24];
+ tt2 ^= tt3;
+ t0 = tt0 ^ tt2;
+
+ tt0 = A[ 2] ^ A[ 7];
+ tt1 = A[12] ^ A[17];
+ tt0 ^= A[22] ^ tt1;
+ tt0 = (tt0 << 1) | (tt0 >>> 63);
+ tt2 = A[ 0] ^ A[ 5];
+ tt3 = A[10] ^ A[15];
+ tt0 ^= A[20];
+ tt2 ^= tt3;
+ t1 = tt0 ^ tt2;
+
+ tt0 = A[ 3] ^ A[ 8];
+ tt1 = A[13] ^ A[18];
+ tt0 ^= A[23] ^ tt1;
+ tt0 = (tt0 << 1) | (tt0 >>> 63);
+ tt2 = A[ 1] ^ A[ 6];
+ tt3 = A[11] ^ A[16];
+ tt0 ^= A[21];
+ tt2 ^= tt3;
+ t2 = tt0 ^ tt2;
+
+ tt0 = A[ 4] ^ A[ 9];
+ tt1 = A[14] ^ A[19];
+ tt0 ^= A[24] ^ tt1;
+ tt0 = (tt0 << 1) | (tt0 >>> 63);
+ tt2 = A[ 2] ^ A[ 7];
+ tt3 = A[12] ^ A[17];
+ tt0 ^= A[22];
+ tt2 ^= tt3;
+ t3 = tt0 ^ tt2;
+
+ tt0 = A[ 0] ^ A[ 5];
+ tt1 = A[10] ^ A[15];
+ tt0 ^= A[20] ^ tt1;
+ tt0 = (tt0 << 1) | (tt0 >>> 63);
+ tt2 = A[ 3] ^ A[ 8];
+ tt3 = A[13] ^ A[18];
+ tt0 ^= A[23];
+ tt2 ^= tt3;
+ t4 = tt0 ^ tt2;
+
+ A[ 0] = A[ 0] ^ t0;
+ A[ 5] = A[ 5] ^ t0;
+ A[10] = A[10] ^ t0;
+ A[15] = A[15] ^ t0;
+ A[20] = A[20] ^ t0;
+ A[ 1] = A[ 1] ^ t1;
+ A[ 6] = A[ 6] ^ t1;
+ A[11] = A[11] ^ t1;
+ A[16] = A[16] ^ t1;
+ A[21] = A[21] ^ t1;
+ A[ 2] = A[ 2] ^ t2;
+ A[ 7] = A[ 7] ^ t2;
+ A[12] = A[12] ^ t2;
+ A[17] = A[17] ^ t2;
+ A[22] = A[22] ^ t2;
+ A[ 3] = A[ 3] ^ t3;
+ A[ 8] = A[ 8] ^ t3;
+ A[13] = A[13] ^ t3;
+ A[18] = A[18] ^ t3;
+ A[23] = A[23] ^ t3;
+ A[ 4] = A[ 4] ^ t4;
+ A[ 9] = A[ 9] ^ t4;
+ A[14] = A[14] ^ t4;
+ A[19] = A[19] ^ t4;
+ A[24] = A[24] ^ t4;
+ A[ 5] = (A[ 5] << 36) | (A[ 5] >>> (64 - 36));
+ A[10] = (A[10] << 3) | (A[10] >>> (64 - 3));
+ A[15] = (A[15] << 41) | (A[15] >>> (64 - 41));
+ A[20] = (A[20] << 18) | (A[20] >>> (64 - 18));
+ A[ 1] = (A[ 1] << 1) | (A[ 1] >>> (64 - 1));
+ A[ 6] = (A[ 6] << 44) | (A[ 6] >>> (64 - 44));
+ A[11] = (A[11] << 10) | (A[11] >>> (64 - 10));
+ A[16] = (A[16] << 45) | (A[16] >>> (64 - 45));
+ A[21] = (A[21] << 2) | (A[21] >>> (64 - 2));
+ A[ 2] = (A[ 2] << 62) | (A[ 2] >>> (64 - 62));
+ A[ 7] = (A[ 7] << 6) | (A[ 7] >>> (64 - 6));
+ A[12] = (A[12] << 43) | (A[12] >>> (64 - 43));
+ A[17] = (A[17] << 15) | (A[17] >>> (64 - 15));
+ A[22] = (A[22] << 61) | (A[22] >>> (64 - 61));
+ A[ 3] = (A[ 3] << 28) | (A[ 3] >>> (64 - 28));
+ A[ 8] = (A[ 8] << 55) | (A[ 8] >>> (64 - 55));
+ A[13] = (A[13] << 25) | (A[13] >>> (64 - 25));
+ A[18] = (A[18] << 21) | (A[18] >>> (64 - 21));
+ A[23] = (A[23] << 56) | (A[23] >>> (64 - 56));
+ A[ 4] = (A[ 4] << 27) | (A[ 4] >>> (64 - 27));
+ A[ 9] = (A[ 9] << 20) | (A[ 9] >>> (64 - 20));
+ A[14] = (A[14] << 39) | (A[14] >>> (64 - 39));
+ A[19] = (A[19] << 8) | (A[19] >>> (64 - 8));
+ A[24] = (A[24] << 14) | (A[24] >>> (64 - 14));
+ bnn = ~A[12];
+ kt = A[ 6] | A[12];
+ c0 = A[ 0] ^ kt;
+ kt = bnn | A[18];
+ c1 = A[ 6] ^ kt;
+ kt = A[18] & A[24];
+ c2 = A[12] ^ kt;
+ kt = A[24] | A[ 0];
+ c3 = A[18] ^ kt;
+ kt = A[ 0] & A[ 6];
+ c4 = A[24] ^ kt;
+ A[ 0] = c0;
+ A[ 6] = c1;
+ A[12] = c2;
+ A[18] = c3;
+ A[24] = c4;
+ bnn = ~A[22];
+ kt = A[ 9] | A[10];
+ c0 = A[ 3] ^ kt;
+ kt = A[10] & A[16];
+ c1 = A[ 9] ^ kt;
+ kt = A[16] | bnn;
+ c2 = A[10] ^ kt;
+ kt = A[22] | A[ 3];
+ c3 = A[16] ^ kt;
+ kt = A[ 3] & A[ 9];
+ c4 = A[22] ^ kt;
+ A[ 3] = c0;
+ A[ 9] = c1;
+ A[10] = c2;
+ A[16] = c3;
+ A[22] = c4;
+ bnn = ~A[19];
+ kt = A[ 7] | A[13];
+ c0 = A[ 1] ^ kt;
+ kt = A[13] & A[19];
+ c1 = A[ 7] ^ kt;
+ kt = bnn & A[20];
+ c2 = A[13] ^ kt;
+ kt = A[20] | A[ 1];
+ c3 = bnn ^ kt;
+ kt = A[ 1] & A[ 7];
+ c4 = A[20] ^ kt;
+ A[ 1] = c0;
+ A[ 7] = c1;
+ A[13] = c2;
+ A[19] = c3;
+ A[20] = c4;
+ bnn = ~A[17];
+ kt = A[ 5] & A[11];
+ c0 = A[ 4] ^ kt;
+ kt = A[11] | A[17];
+ c1 = A[ 5] ^ kt;
+ kt = bnn | A[23];
+ c2 = A[11] ^ kt;
+ kt = A[23] & A[ 4];
+ c3 = bnn ^ kt;
+ kt = A[ 4] | A[ 5];
+ c4 = A[23] ^ kt;
+ A[ 4] = c0;
+ A[ 5] = c1;
+ A[11] = c2;
+ A[17] = c3;
+ A[23] = c4;
+ bnn = ~A[ 8];
+ kt = bnn & A[14];
+ c0 = A[ 2] ^ kt;
+ kt = A[14] | A[15];
+ c1 = bnn ^ kt;
+ kt = A[15] & A[21];
+ c2 = A[14] ^ kt;
+ kt = A[21] | A[ 2];
+ c3 = A[15] ^ kt;
+ kt = A[ 2] & A[ 8];
+ c4 = A[21] ^ kt;
+ A[ 2] = c0;
+ A[ 8] = c1;
+ A[14] = c2;
+ A[15] = c3;
+ A[21] = c4;
+ A[ 0] = A[ 0] ^ RC[j + 0];
+
+ tt0 = A[ 6] ^ A[ 9];
+ tt1 = A[ 7] ^ A[ 5];
+ tt0 ^= A[ 8] ^ tt1;
+ tt0 = (tt0 << 1) | (tt0 >>> 63);
+ tt2 = A[24] ^ A[22];
+ tt3 = A[20] ^ A[23];
+ tt0 ^= A[21];
+ tt2 ^= tt3;
+ t0 = tt0 ^ tt2;
+
+ tt0 = A[12] ^ A[10];
+ tt1 = A[13] ^ A[11];
+ tt0 ^= A[14] ^ tt1;
+ tt0 = (tt0 << 1) | (tt0 >>> 63);
+ tt2 = A[ 0] ^ A[ 3];
+ tt3 = A[ 1] ^ A[ 4];
+ tt0 ^= A[ 2];
+ tt2 ^= tt3;
+ t1 = tt0 ^ tt2;
+
+ tt0 = A[18] ^ A[16];
+ tt1 = A[19] ^ A[17];
+ tt0 ^= A[15] ^ tt1;
+ tt0 = (tt0 << 1) | (tt0 >>> 63);
+ tt2 = A[ 6] ^ A[ 9];
+ tt3 = A[ 7] ^ A[ 5];
+ tt0 ^= A[ 8];
+ tt2 ^= tt3;
+ t2 = tt0 ^ tt2;
+
+ tt0 = A[24] ^ A[22];
+ tt1 = A[20] ^ A[23];
+ tt0 ^= A[21] ^ tt1;
+ tt0 = (tt0 << 1) | (tt0 >>> 63);
+ tt2 = A[12] ^ A[10];
+ tt3 = A[13] ^ A[11];
+ tt0 ^= A[14];
+ tt2 ^= tt3;
+ t3 = tt0 ^ tt2;
+
+ tt0 = A[ 0] ^ A[ 3];
+ tt1 = A[ 1] ^ A[ 4];
+ tt0 ^= A[ 2] ^ tt1;
+ tt0 = (tt0 << 1) | (tt0 >>> 63);
+ tt2 = A[18] ^ A[16];
+ tt3 = A[19] ^ A[17];
+ tt0 ^= A[15];
+ tt2 ^= tt3;
+ t4 = tt0 ^ tt2;
+
+ A[ 0] = A[ 0] ^ t0;
+ A[ 3] = A[ 3] ^ t0;
+ A[ 1] = A[ 1] ^ t0;
+ A[ 4] = A[ 4] ^ t0;
+ A[ 2] = A[ 2] ^ t0;
+ A[ 6] = A[ 6] ^ t1;
+ A[ 9] = A[ 9] ^ t1;
+ A[ 7] = A[ 7] ^ t1;
+ A[ 5] = A[ 5] ^ t1;
+ A[ 8] = A[ 8] ^ t1;
+ A[12] = A[12] ^ t2;
+ A[10] = A[10] ^ t2;
+ A[13] = A[13] ^ t2;
+ A[11] = A[11] ^ t2;
+ A[14] = A[14] ^ t2;
+ A[18] = A[18] ^ t3;
+ A[16] = A[16] ^ t3;
+ A[19] = A[19] ^ t3;
+ A[17] = A[17] ^ t3;
+ A[15] = A[15] ^ t3;
+ A[24] = A[24] ^ t4;
+ A[22] = A[22] ^ t4;
+ A[20] = A[20] ^ t4;
+ A[23] = A[23] ^ t4;
+ A[21] = A[21] ^ t4;
+ A[ 3] = (A[ 3] << 36) | (A[ 3] >>> (64 - 36));
+ A[ 1] = (A[ 1] << 3) | (A[ 1] >>> (64 - 3));
+ A[ 4] = (A[ 4] << 41) | (A[ 4] >>> (64 - 41));
+ A[ 2] = (A[ 2] << 18) | (A[ 2] >>> (64 - 18));
+ A[ 6] = (A[ 6] << 1) | (A[ 6] >>> (64 - 1));
+ A[ 9] = (A[ 9] << 44) | (A[ 9] >>> (64 - 44));
+ A[ 7] = (A[ 7] << 10) | (A[ 7] >>> (64 - 10));
+ A[ 5] = (A[ 5] << 45) | (A[ 5] >>> (64 - 45));
+ A[ 8] = (A[ 8] << 2) | (A[ 8] >>> (64 - 2));
+ A[12] = (A[12] << 62) | (A[12] >>> (64 - 62));
+ A[10] = (A[10] << 6) | (A[10] >>> (64 - 6));
+ A[13] = (A[13] << 43) | (A[13] >>> (64 - 43));
+ A[11] = (A[11] << 15) | (A[11] >>> (64 - 15));
+ A[14] = (A[14] << 61) | (A[14] >>> (64 - 61));
+ A[18] = (A[18] << 28) | (A[18] >>> (64 - 28));
+ A[16] = (A[16] << 55) | (A[16] >>> (64 - 55));
+ A[19] = (A[19] << 25) | (A[19] >>> (64 - 25));
+ A[17] = (A[17] << 21) | (A[17] >>> (64 - 21));
+ A[15] = (A[15] << 56) | (A[15] >>> (64 - 56));
+ A[24] = (A[24] << 27) | (A[24] >>> (64 - 27));
+ A[22] = (A[22] << 20) | (A[22] >>> (64 - 20));
+ A[20] = (A[20] << 39) | (A[20] >>> (64 - 39));
+ A[23] = (A[23] << 8) | (A[23] >>> (64 - 8));
+ A[21] = (A[21] << 14) | (A[21] >>> (64 - 14));
+ bnn = ~A[13];
+ kt = A[ 9] | A[13];
+ c0 = A[ 0] ^ kt;
+ kt = bnn | A[17];
+ c1 = A[ 9] ^ kt;
+ kt = A[17] & A[21];
+ c2 = A[13] ^ kt;
+ kt = A[21] | A[ 0];
+ c3 = A[17] ^ kt;
+ kt = A[ 0] & A[ 9];
+ c4 = A[21] ^ kt;
+ A[ 0] = c0;
+ A[ 9] = c1;
+ A[13] = c2;
+ A[17] = c3;
+ A[21] = c4;
+ bnn = ~A[14];
+ kt = A[22] | A[ 1];
+ c0 = A[18] ^ kt;
+ kt = A[ 1] & A[ 5];
+ c1 = A[22] ^ kt;
+ kt = A[ 5] | bnn;
+ c2 = A[ 1] ^ kt;
+ kt = A[14] | A[18];
+ c3 = A[ 5] ^ kt;
+ kt = A[18] & A[22];
+ c4 = A[14] ^ kt;
+ A[18] = c0;
+ A[22] = c1;
+ A[ 1] = c2;
+ A[ 5] = c3;
+ A[14] = c4;
+ bnn = ~A[23];
+ kt = A[10] | A[19];
+ c0 = A[ 6] ^ kt;
+ kt = A[19] & A[23];
+ c1 = A[10] ^ kt;
+ kt = bnn & A[ 2];
+ c2 = A[19] ^ kt;
+ kt = A[ 2] | A[ 6];
+ c3 = bnn ^ kt;
+ kt = A[ 6] & A[10];
+ c4 = A[ 2] ^ kt;
+ A[ 6] = c0;
+ A[10] = c1;
+ A[19] = c2;
+ A[23] = c3;
+ A[ 2] = c4;
+ bnn = ~A[11];
+ kt = A[ 3] & A[ 7];
+ c0 = A[24] ^ kt;
+ kt = A[ 7] | A[11];
+ c1 = A[ 3] ^ kt;
+ kt = bnn | A[15];
+ c2 = A[ 7] ^ kt;
+ kt = A[15] & A[24];
+ c3 = bnn ^ kt;
+ kt = A[24] | A[ 3];
+ c4 = A[15] ^ kt;
+ A[24] = c0;
+ A[ 3] = c1;
+ A[ 7] = c2;
+ A[11] = c3;
+ A[15] = c4;
+ bnn = ~A[16];
+ kt = bnn & A[20];
+ c0 = A[12] ^ kt;
+ kt = A[20] | A[ 4];
+ c1 = bnn ^ kt;
+ kt = A[ 4] & A[ 8];
+ c2 = A[20] ^ kt;
+ kt = A[ 8] | A[12];
+ c3 = A[ 4] ^ kt;
+ kt = A[12] & A[16];
+ c4 = A[ 8] ^ kt;
+ A[12] = c0;
+ A[16] = c1;
+ A[20] = c2;
+ A[ 4] = c3;
+ A[ 8] = c4;
+ A[ 0] = A[ 0] ^ RC[j + 1];
+ t = A[ 5];
+ A[ 5] = A[18];
+ A[18] = A[11];
+ A[11] = A[10];
+ A[10] = A[ 6];
+ A[ 6] = A[22];
+ A[22] = A[20];
+ A[20] = A[12];
+ A[12] = A[19];
+ A[19] = A[15];
+ A[15] = A[24];
+ A[24] = A[ 8];
+ A[ 8] = t;
+ t = A[ 1];
+ A[ 1] = A[ 9];
+ A[ 9] = A[14];
+ A[14] = A[ 2];
+ A[ 2] = A[13];
+ A[13] = A[23];
+ A[23] = A[ 4];
+ A[ 4] = A[21];
+ A[21] = A[16];
+ A[16] = A[ 3];
+ A[ 3] = A[17];
+ A[17] = A[ 7];
+ A[ 7] = t;
+ }
+ }
+
+ protected void doPadding(byte[] out, int off)
+ {
+ int ptr = flush();
+ byte[] buf = getBlockBuffer();
+ if ((ptr + 1) == buf.length) {
+ buf[ptr] = (byte)0x81;
+ } else {
+ buf[ptr] = (byte)0x01;
+ for (int i = ptr + 1; i < (buf.length - 1); i ++)
+ buf[i] = 0;
+ buf[buf.length - 1] = (byte)0x80;
+ }
+ processBlock(buf);
+ A[ 1] = ~A[ 1];
+ A[ 2] = ~A[ 2];
+ A[ 8] = ~A[ 8];
+ A[12] = ~A[12];
+ A[17] = ~A[17];
+ A[20] = ~A[20];
+ int dlen = engineGetDigestLength();
+ for (int i = 0; i < dlen; i += 8)
+ encodeLELong(A[i >>> 3], tmpOut, i);
+ System.arraycopy(tmpOut, 0, out, off, dlen);
+ }
+
+ protected void doInit()
+ {
+ A = new long[25];
+ tmpOut = new byte[(engineGetDigestLength() + 7) & ~7];
+ doReset();
+ }
+
+ public int getBlockLength()
+ {
+ return 200 - 2 * engineGetDigestLength();
+ }
+
+ private final void doReset()
+ {
+ for (int i = 0; i < 25; i ++)
+ A[i] = 0;
+ A[ 1] = 0xFFFFFFFFFFFFFFFFL;
+ A[ 2] = 0xFFFFFFFFFFFFFFFFL;
+ A[ 8] = 0xFFFFFFFFFFFFFFFFL;
+ A[12] = 0xFFFFFFFFFFFFFFFFL;
+ A[17] = 0xFFFFFFFFFFFFFFFFL;
+ A[20] = 0xFFFFFFFFFFFFFFFFL;
+ }
+
+ protected Digest copyState(KeccakCore dst)
+ {
+ System.arraycopy(A, 0, dst.A, 0, 25);
+ return super.copyState(dst);
+ }
+
+ public String toString()
+ {
+ return "Keccak-" + (engineGetDigestLength() << 3);
+ }
+}
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/models/AccountState.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/models/AccountState.kt
new file mode 100644
index 00000000..719338b3
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/models/AccountState.kt
@@ -0,0 +1,16 @@
+package io.horizontalsystems.ethereumkit.light.models
+
+import io.horizontalsystems.ethereumkit.core.toHexString
+import java.math.BigInteger
+
+class AccountState(val address: ByteArray, val nonce: Long, val balance: BigInteger, val storageHash: ByteArray, val codeHash: ByteArray) {
+
+ override fun toString(): String {
+ return "(\n" +
+ " nonce: $nonce\n" +
+ " balance: $balance\n" +
+ " storageHash: ${storageHash.toHexString()}\n" +
+ " codeHash: ${codeHash.toHexString()}\n" +
+ ")"
+ }
+}
\ No newline at end of file
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/models/BlockHeader.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/models/BlockHeader.kt
new file mode 100644
index 00000000..7c1b0923
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/models/BlockHeader.kt
@@ -0,0 +1,115 @@
+package io.horizontalsystems.ethereumkit.light.models
+
+import io.horizontalsystems.ethereumkit.core.toHexString
+import io.horizontalsystems.ethereumkit.light.crypto.CryptoUtils
+import io.horizontalsystems.ethereumkit.light.rlp.RLP
+import io.horizontalsystems.ethereumkit.light.rlp.RLPList
+import io.horizontalsystems.ethereumkit.light.toBigInteger
+import io.horizontalsystems.ethereumkit.light.toLong
+import java.math.BigInteger
+
+class BlockHeader {
+
+ companion object {
+ val EMPTY_TRIE_HASH = CryptoUtils.sha3(RLP.encodeElement(ByteArray(0)))
+ }
+
+ val hashHex: ByteArray
+ var totalDifficulty: ByteArray = byteArrayOf() // Scalar value corresponding to the sum of difficulty values of all previous blocks
+ val parentHash: ByteArray // 256-bit Keccak-256 hash of parent block
+ val unclesHash: ByteArray // 256-bit Keccak-256 hash of uncles portion of this block
+ val coinbase: ByteArray // 160-bit address for fees collected from successful mining
+ val stateRoot: ByteArray // 256-bit state trie root hash
+ val transactionsRoot: ByteArray // 256-bit transactions trie root hash
+ val receiptsRoot: ByteArray // 256-bit receipts trie root hash
+ val logsBloom: ByteArray /* The Bloom filter composed from indexable information
+ * (logger address and log topics) contained in each log entry
+ * from the receipt of each transaction in the transactions list */
+ val difficulty: ByteArray /* A scalar value corresponding to the difficulty level of this block.
+ * This can be calculated from the previous block’s difficulty level
+ * and the timestamp */
+ val height: BigInteger
+ val gasLimit: ByteArray // A scalar value equal to the current limit of gas expenditure per block
+ val gasUsed: Long // A scalar value equal to the total gas used in transactions in this block
+ val timestamp: Long // A scalar value equal to the reasonable output of Unix's time() at this block's inception
+ val extraData: ByteArray /* An arbitrary byte array containing data relevant to this block.
+ * With the exception of the genesis block, this must be 32 bytes or fewer */
+ val mixHash: ByteArray /* A 256-bit hash which proves that together with nonce a sufficient amount
+ * of computation has been carried out on this block */
+ val nonce: ByteArray /* A 64-bit hash which proves that a sufficient amount
+ * of computation has been carried out on this block */
+
+ constructor(
+ hashHex: ByteArray,
+ totalDifficulty: ByteArray, // Scalar value corresponding to the sum of difficulty values of all previous blocks
+ parentHash: ByteArray, // 256-bit Keccak-256 hash of parent block
+ unclesHash: ByteArray, // 256-bit Keccak-256 hash of uncles portion of this block
+ coinbase: ByteArray, // 160-bit address for fees collected from successful mining
+ stateRoot: ByteArray, // 256-bit state trie root hash
+ transactionsRoot: ByteArray, // 256-bit transactions trie root hash
+ receiptsRoot: ByteArray, // 256-bit receipts trie root hash
+ logsBloom: ByteArray, /* The Bloom filter composed from indexable information
+ * (logger address and log topics) contained in each log entry
+ * from the receipt of each transaction in the transactions list */
+ difficulty: ByteArray, /* A scalar value corresponding to the difficulty level of this block.
+ * This can be calculated from the previous block’s difficulty level
+ * and the timestamp */
+ height: BigInteger,
+ gasLimit: ByteArray, // A scalar value equal to the current limit of gas expenditure per block
+ gasUsed: Long, // A scalar value equal to the total gas used in transactions in this block
+ timestamp: Long, // A scalar value equal to the reasonable output of Unix's time() at this block's inception
+ extraData: ByteArray, /* An arbitrary byte array containing data relevant to this block.
+ * With the exception of the genesis block, this must be 32 bytes or fewer */
+ mixHash: ByteArray, /* A 256-bit hash which proves that together with nonce a sufficient amount
+ * of computation has been carried out on this block */
+ nonce: ByteArray /* A 64-bit hash which proves that a sufficient amount
+ * of computation has been carried out on this block */
+ ) {
+ this.hashHex = hashHex
+ this.totalDifficulty = totalDifficulty
+ this.parentHash = parentHash
+ this.unclesHash = unclesHash
+ this.coinbase = coinbase
+ this.stateRoot = stateRoot
+ this.transactionsRoot = transactionsRoot
+ this.receiptsRoot = receiptsRoot
+ this.logsBloom = logsBloom
+ this.difficulty = difficulty
+ this.height = height
+ this.gasLimit = gasLimit
+ this.gasUsed = gasUsed
+ this.timestamp = timestamp
+ this.extraData = extraData
+ this.mixHash = mixHash
+ this.nonce = nonce
+ }
+
+ constructor(rlpHeader: RLPList) {
+
+ this.hashHex = CryptoUtils.sha3(rlpHeader.rlpData ?: ByteArray(0))
+ this.parentHash = rlpHeader[0].rlpData ?: byteArrayOf()
+ this.unclesHash = rlpHeader[1].rlpData ?: byteArrayOf()
+ this.coinbase = rlpHeader[2].rlpData ?: byteArrayOf()
+ this.stateRoot = rlpHeader[3].rlpData ?: byteArrayOf()
+
+ val txsRoot = rlpHeader[4].rlpData
+ this.transactionsRoot = if (txsRoot == null || txsRoot.isEmpty()) EMPTY_TRIE_HASH else txsRoot
+
+ val rcptsRoot = rlpHeader[5].rlpData
+ this.receiptsRoot = if (rcptsRoot == null || rcptsRoot.isEmpty()) EMPTY_TRIE_HASH else rcptsRoot
+
+ this.logsBloom = rlpHeader[6].rlpData ?: byteArrayOf()
+ this.difficulty = rlpHeader[7].rlpData ?: byteArrayOf()
+ this.height = rlpHeader[8].rlpData.toBigInteger()
+ this.gasLimit = rlpHeader[9].rlpData ?: byteArrayOf()
+ this.gasUsed = rlpHeader[10].rlpData.toLong()
+ this.timestamp = rlpHeader[11].rlpData.toLong()
+ this.extraData = rlpHeader[12].rlpData ?: byteArrayOf()
+ this.mixHash = rlpHeader[13].rlpData ?: byteArrayOf()
+ this.nonce = rlpHeader[14].rlpData ?: byteArrayOf()
+ }
+
+ override fun toString(): String {
+ return "(hash: ${hashHex.toHexString()}; height: $height; parentHash: ${parentHash.toHexString()})"
+ }
+}
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/IMessage.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/IMessage.kt
new file mode 100644
index 00000000..b53e6baf
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/IMessage.kt
@@ -0,0 +1,10 @@
+package io.horizontalsystems.ethereumkit.light.net
+
+interface IMessage {
+ var code: Int
+ fun encoded(): ByteArray
+}
+
+interface IP2PMessage : IMessage
+
+interface ILESMessage : IMessage
\ No newline at end of file
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/INetwork.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/INetwork.kt
new file mode 100644
index 00000000..20856896
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/INetwork.kt
@@ -0,0 +1,9 @@
+package io.horizontalsystems.ethereumkit.light.net
+
+import io.horizontalsystems.ethereumkit.light.models.BlockHeader
+
+interface INetwork {
+ val id: Int
+ val genesisBlockHash: ByteArray
+ val checkpointBlock: BlockHeader
+}
\ No newline at end of file
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/Node.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/Node.kt
new file mode 100644
index 00000000..ff88b3d2
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/Node.kt
@@ -0,0 +1,6 @@
+package io.horizontalsystems.ethereumkit.light.net
+
+class Node(val id: ByteArray,
+ val host: String,
+ val port: Int,
+ val discoveryPort: Int)
\ No newline at end of file
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/PeerGroup.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/PeerGroup.kt
new file mode 100644
index 00000000..1abfb4b8
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/PeerGroup.kt
@@ -0,0 +1,90 @@
+package io.horizontalsystems.ethereumkit.light.net
+
+import io.horizontalsystems.ethereumkit.core.hexStringToByteArray
+import io.horizontalsystems.ethereumkit.light.crypto.CryptoUtils
+import io.horizontalsystems.ethereumkit.light.models.BlockHeader
+import io.horizontalsystems.ethereumkit.light.net.les.IPeerListener
+import io.horizontalsystems.ethereumkit.light.net.les.Peer
+import io.horizontalsystems.ethereumkit.light.net.les.messages.ProofsMessage
+import java.math.BigInteger
+
+class PeerGroup(network: INetwork, address: String) : IPeerListener {
+
+
+ private var syncPeer: Peer
+ private var blockHeaders = mutableListOf()
+ private var address: ByteArray
+
+ init {
+ val myKey = CryptoUtils.ecKeyFromPrivate(BigInteger("38208918395832628331087730025239389699013035341486183519748173810236817397977"))
+
+ /*val node = Node(id = "1baf02c18c08ab0d009ccc9b51168be6a8776509ff229a6ca08507b53579cb99e0df1709bd1bcf64aed348f9a31298842cf12c1764c8de9d28abb921a548ad8c".hexStringToByteArray(),
+ host = "eth-testnet.horizontalsystems.xyz",
+ port = 20303,
+ discoveryPort = 30301)*/
+
+ val node = Node(id = "e679038c2e4f9f764acd788c3935cf526f7f630b55254a122452e63e2cfae3066ca6b6c44082c2dfbe9ddffc9df80546d40ef38a0e3dfc9c8720c732446ca8f3".hexStringToByteArray(),
+ host = "192.168.4.39",
+ port = 30303,
+ discoveryPort = 30301)
+
+// this.address = address.substring(2).hexStringToByteArray()
+ this.address = "f757461bdc25ee2b047d545a50768e52d530b750".hexStringToByteArray()
+// this.address = "f757461bdc25ee2b047d545a50768e52d530b751".hexStringToByteArray()
+// this.address = "37531e574427BDE92d9B3a3c2291D9A004827435".hexStringToByteArray()
+// this.address = "1b763c4b9632d6876D83B2270fF4d01b792DE479".hexStringToByteArray()
+// this.address = "401CB37eFa5d82dC51FB599e6A4B1D2b3aaeb2B2".hexStringToByteArray()
+
+ syncPeer = Peer(network, network.checkpointBlock, myKey, node, this)
+ blockHeaders.add(network.checkpointBlock)
+ }
+
+ //------------------Public methods----------------------
+
+ fun start() {
+ syncPeer.connect()
+ }
+
+ fun syncBlocks() {
+ syncPeer.downloadBlocksFrom(blockHeaders.last())
+ }
+
+ //-----------------IPeerListener methods----------------
+
+ override fun connected() {
+ println("PeerGroup -> connected\n")
+ syncBlocks()
+ }
+
+ override fun blocksReceived(blockHeaders: List) {
+ println("PeerGroup -> blocksReceived\n")
+
+ if (blockHeaders.size < 2) {
+ println("blocks synced!\n")
+
+ this.blockHeaders.lastOrNull()?.let { lastBlock ->
+ syncPeer.getBalance(address, lastBlock.hashHex)
+ }
+
+ return
+ }
+
+ this.blockHeaders.addAll(blockHeaders)
+ syncBlocks()
+ }
+
+ override fun proofReceived(message: ProofsMessage) {
+ println("PeerGroup -> blocksReceived\n")
+
+ val lastBlock = blockHeaders.last()
+
+ try {
+ val state = message.getValidatedState(lastBlock.stateRoot, address)
+ println(state)
+ } catch (ex: Exception) {
+ ex.printStackTrace()
+ println("proof error: $ex")
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/Ropsten.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/Ropsten.kt
new file mode 100644
index 00000000..3434f1f2
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/Ropsten.kt
@@ -0,0 +1,32 @@
+package io.horizontalsystems.ethereumkit.light.net
+
+import io.horizontalsystems.ethereumkit.core.hexStringToByteArray
+import io.horizontalsystems.ethereumkit.light.models.BlockHeader
+import java.math.BigInteger
+
+class Ropsten : INetwork {
+
+ override val id: Int = 3
+
+ override val genesisBlockHash: ByteArray =
+ "41941023680923e0fe4d74a34bdac8141f2540e3ae90623718e47d66d1ca4a2d".hexStringToByteArray()
+
+ override val checkpointBlock =
+ BlockHeader(hashHex = "8e979e196f08a06ecd3e7bbbf83b387a5e429b43a6694df7b01b9402a272eec6".hexStringToByteArray(),
+ totalDifficulty = "18284610994619994".hexStringToByteArray(),
+ parentHash = "91690d0990e80aa73341926434746bb532194204d81b0736cc5f147a60c0824f".hexStringToByteArray(),
+ unclesHash = "1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347".hexStringToByteArray(),
+ coinbase = "b17fc44dd79d21cd7f4d8c9686c98ae9039b3909".hexStringToByteArray(),
+ stateRoot = "a44e837fc749e7fb4dea349c37f6c4d0c2306fe7452d3865e4e75179bdf54e8c".hexStringToByteArray(),
+ transactionsRoot = "b3a7a2911892b1e26a8beadf9931861a66227698e352a976af8a00948bc9d547".hexStringToByteArray(),
+ receiptsRoot = "1b6d1dcc3549c4dbc1c211b17d075457a9930cd9effd82e68a21742350590782".hexStringToByteArray(),
+ logsBloom = "00000000000042000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000200000000008000000000000000040000000000000000000000000000000000000000000000000000000000010000000000000000010000000000000000000000000000000100000000000000001000000010000000000000000080000000000000000000000000004000010000000000200000000000000000000000002000000000100000000000010000000000000000000000000002000000000000000000000000000000000000000004000000000000100000004000000".hexStringToByteArray(),
+ difficulty = "21F51199".hexStringToByteArray(),
+ height = BigInteger("5049204"),
+ gasLimit = "7A121D".hexStringToByteArray(),
+ gasUsed = 1098844,
+ timestamp = 1550569079,
+ extraData = "de830203018f5061726974792d457468657265756d86312e33312e31826c69".hexStringToByteArray(),
+ mixHash = "d95fe97e78ae762fbccf683c433fcfa72b137757e602dffd5e8e26b3ba3a02f8".hexStringToByteArray(),
+ nonce = "2bb183a1640b7c81".hexStringToByteArray())
+}
\ No newline at end of file
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/connection/AESCipher.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/connection/AESCipher.kt
new file mode 100644
index 00000000..b51de676
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/connection/AESCipher.kt
@@ -0,0 +1,24 @@
+package io.horizontalsystems.ethereumkit.light.net.connection
+
+import org.spongycastle.crypto.StreamCipher
+import org.spongycastle.crypto.engines.AESEngine
+import org.spongycastle.crypto.modes.SICBlockCipher
+import org.spongycastle.crypto.params.KeyParameter
+import org.spongycastle.crypto.params.ParametersWithIV
+
+class AESCipher(val key: ByteArray, forEncryption: Boolean ) {
+
+ private val cipher: StreamCipher
+
+ init {
+ val encAesEngine = AESEngine()
+ cipher = SICBlockCipher(encAesEngine)
+ cipher.init(forEncryption, ParametersWithIV(KeyParameter(key), ByteArray(encAesEngine.blockSize)))
+ }
+
+ fun process(data: ByteArray): ByteArray {
+ val result = ByteArray(data.size)
+ cipher.processBytes(data, 0, data.size, result, 0)
+ return result
+ }
+}
\ No newline at end of file
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/connection/Connection.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/connection/Connection.kt
new file mode 100644
index 00000000..b74a7d98
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/connection/Connection.kt
@@ -0,0 +1,178 @@
+package io.horizontalsystems.ethereumkit.light.net.connection
+
+import io.horizontalsystems.ethereumkit.light.RandomUtils
+import io.horizontalsystems.ethereumkit.light.crypto.CryptoUtils
+import io.horizontalsystems.ethereumkit.light.crypto.CryptoUtils.CURVE
+import io.horizontalsystems.ethereumkit.light.crypto.ECIESEncryptedMessage
+import io.horizontalsystems.ethereumkit.light.crypto.ECKey
+import io.horizontalsystems.ethereumkit.light.net.IMessage
+import io.horizontalsystems.ethereumkit.light.net.Node
+import io.horizontalsystems.ethereumkit.light.net.devp2p.Capability
+import io.horizontalsystems.ethereumkit.light.toShort
+import org.spongycastle.math.ec.ECPoint
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+import java.net.ConnectException
+import java.net.InetSocketAddress
+import java.net.Socket
+import java.net.SocketTimeoutException
+import java.util.concurrent.ArrayBlockingQueue
+import java.util.concurrent.BlockingQueue
+import java.util.concurrent.TimeUnit
+import java.util.logging.Logger
+
+
+interface IPeerConnectionListener {
+ fun connectionKey(): ECKey
+ fun onConnectionEstablished()
+ fun onDisconnected(error: Throwable?)
+ fun onMessageReceived(message: IMessage)
+}
+
+interface IPeerConnection {
+ val listener: IPeerConnectionListener
+ val logName: String
+ fun connect()
+ fun disconnect(error: Throwable?)
+ fun send(message: IMessage)
+ fun register(capabilities: List)
+}
+
+class Connection(private val node: Node, override val listener: IPeerConnectionListener) : IPeerConnection, Thread() {
+ override val logName: String = "${node.id}@${node.host}:${node.port}"
+
+ private val logger = Logger.getLogger("Peer[${node.host}]")
+ private val sendingQueue: BlockingQueue = ArrayBlockingQueue(100)
+ private val socket = Socket()
+
+ @Volatile
+ private var isRunning = false
+
+ private lateinit var handshake: EncryptionHandshake
+ private lateinit var frameCodec: FrameCodec
+ private val frameHandler: FrameHandler = FrameHandler()
+
+ private val remotePublicKeyPoint: ECPoint
+ by lazy {
+ val remotePublicKey = ByteArray(65)
+ val nodeId = this.node.id
+ System.arraycopy(nodeId, 0, remotePublicKey, 1, nodeId.size)
+ remotePublicKey[0] = 0x04
+ CURVE.curve.decodePoint(remotePublicKey)
+ }
+
+ init {
+ isDaemon = true
+ }
+
+ override fun connect() {
+ start()
+ }
+
+ override fun disconnect(error: Throwable?) {
+ TODO("not implemented")
+ }
+
+ override fun send(message: IMessage) {
+ println(">>>>> $message\n")
+
+ sendingQueue.put(message)
+ }
+
+ override fun register(capabilities: List) {
+ frameHandler.addCapabilities(capabilities)
+ }
+
+ private fun initiateHandshake(outputStream: OutputStream) {
+ handshake = EncryptionHandshake(listener.connectionKey(), remotePublicKeyPoint, CryptoUtils, RandomUtils)
+
+ val authMessagePackets = handshake.createAuthMessage()
+
+ outputStream.write(authMessagePackets)
+ }
+
+ private fun handleAuthAckMessage(inputsStream: InputStream): Secrets {
+ val prefixBytes = ByteArray(2)
+ inputsStream.read(prefixBytes)
+
+ val size = prefixBytes.toShort()
+ val messagePackets = ByteArray(size.toInt())
+
+ inputsStream.read(messagePackets)
+
+ return handshake.handleAuthAckMessage(ECIESEncryptedMessage.decode(prefixBytes + messagePackets))
+ }
+
+ override fun run() {
+ isRunning = true
+ // connect:
+ socket.connect(InetSocketAddress(node.host, node.port), 10000)
+ socket.soTimeout = 10000
+
+ val inputStream = socket.getInputStream()
+ val outputStream = socket.getOutputStream()
+
+ try {
+
+ logger.info("Socket ${node.host} connected.")
+
+ initiateHandshake(outputStream)
+
+ val secrets = handleAuthAckMessage(inputStream)
+
+ frameCodec = FrameCodec(secrets)
+
+ listener.onConnectionEstablished()
+
+ while (isRunning) {
+
+ val msg = sendingQueue.poll(1, TimeUnit.SECONDS)
+ if (isRunning && msg != null) {
+ frameHandler.getFrames(msg).forEach { frame ->
+ frameCodec.writeFrame(frame, outputStream)
+ }
+ }
+
+ while (isRunning && inputStream.available() > 0) {
+ val frame = frameCodec.readFrame(inputStream)
+ if (frame == null) {
+ println("Frame is NULL")
+ } else {
+ frameHandler.addFrame(frame)
+
+ var message = frameHandler.getMessage()
+ while (message != null) {
+ listener.onMessageReceived(message)
+ message = frameHandler.getMessage()
+ }
+ }
+ }
+ }
+
+ } catch (e: SocketTimeoutException) {
+ logger.warning("Socket timeout exception: ${e.message}")
+ listener.onDisconnected(e)
+ } catch (e: ConnectException) {
+ logger.warning("Connect exception: ${e.message}")
+ listener.onDisconnected(e)
+ } catch (e: IOException) {
+ logger.warning("IOException: ${e.message}")
+ listener.onDisconnected(e)
+ } catch (e: InterruptedException) {
+ logger.warning("Peer connection thread interrupted: ${e.message}")
+ listener.onDisconnected(e)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ logger.warning("Peer connection exception: ${e.message}")
+ listener.onDisconnected(e)
+ } finally {
+ isRunning = false
+
+ inputStream.close()
+ outputStream.close()
+
+ socket.close()
+ }
+ }
+}
\ No newline at end of file
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/connection/EncryptionHandshake.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/connection/EncryptionHandshake.kt
new file mode 100644
index 00000000..2922acf3
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/connection/EncryptionHandshake.kt
@@ -0,0 +1,81 @@
+package io.horizontalsystems.ethereumkit.light.net.connection
+
+import io.horizontalsystems.ethereumkit.light.RandomUtils
+import io.horizontalsystems.ethereumkit.light.crypto.CryptoUtils
+import io.horizontalsystems.ethereumkit.light.crypto.ECIESEncryptedMessage
+import io.horizontalsystems.ethereumkit.light.crypto.ECKey
+import io.horizontalsystems.ethereumkit.light.net.connection.messages.AuthAckMessage
+import io.horizontalsystems.ethereumkit.light.net.connection.messages.AuthMessage
+import io.horizontalsystems.ethereumkit.light.xor
+import org.spongycastle.crypto.digests.KeccakDigest
+import org.spongycastle.math.ec.ECPoint
+
+class EncryptionHandshake(private val myKey: ECKey, private val remotePublicKeyPoint: ECPoint, private val cryptoUtils: CryptoUtils, private val randomUtils: RandomUtils) {
+
+ open class HandshakeError : Exception() {
+ class InvalidAuthAckPayload : HandshakeError()
+ }
+
+ companion object {
+ const val MAC_SIZE = 256
+ }
+
+ private var initiatorNonce: ByteArray = randomUtils.randomBytes(32)
+ private var ephemeralKey = randomUtils.randomECKey()
+ private var authMessagePacket: ByteArray = ByteArray(0)
+
+ fun createAuthMessage(): ByteArray {
+ val sharedSecret = cryptoUtils.ecdhAgree(myKey, remotePublicKeyPoint)
+
+ val toBeSigned = sharedSecret.xor(initiatorNonce)
+ val signature = cryptoUtils.ellipticSign(toBeSigned, ephemeralKey)
+
+ val message = AuthMessage(signature, myKey.publicKeyPoint, initiatorNonce)
+
+ authMessagePacket = encrypt(message)
+
+ return authMessagePacket
+ }
+
+ fun handleAuthAckMessage(eciesEncryptedMessage: ECIESEncryptedMessage): Secrets {
+
+ val decrypted = cryptoUtils.eciesDecrypt(myKey.privateKey, eciesEncryptedMessage)
+
+ val authAckMessage = try {
+ AuthAckMessage(decrypted)
+ } catch (ex: Exception) {
+ throw HandshakeError.InvalidAuthAckPayload()
+ }
+
+ return agreeSecret(authAckMessage, eciesEncryptedMessage.encoded())
+ }
+
+ private fun agreeSecret(authAckMessage: AuthAckMessage, authAckMessagePacket: ByteArray): Secrets {
+ val agreedSecret = cryptoUtils.ecdhAgree(ephemeralKey, authAckMessage.ephemPublicKeyPoint)
+ val sharedSecret = cryptoUtils.sha3(agreedSecret + cryptoUtils.sha3(authAckMessage.nonce + initiatorNonce))
+ val secretsAes = cryptoUtils.sha3(agreedSecret + sharedSecret)
+
+ val secretsMac = cryptoUtils.sha3(agreedSecret + secretsAes)
+ val secretsToken = cryptoUtils.sha3(sharedSecret)
+
+ val secretsEgressMac = KeccakDigest(MAC_SIZE)
+ secretsEgressMac.update(secretsMac.xor(authAckMessage.nonce), 0, secretsMac.size)
+ secretsEgressMac.update(authMessagePacket, 0, authMessagePacket.size)
+
+ val secretsIngressMac = KeccakDigest(MAC_SIZE)
+ secretsIngressMac.update(secretsMac.xor(initiatorNonce), 0, secretsMac.size)
+ secretsIngressMac.update(authAckMessagePacket, 0, authAckMessagePacket.size)
+
+ return Secrets(secretsAes, secretsMac, secretsToken, secretsEgressMac, secretsIngressMac)
+ }
+
+ private fun encrypt(message: AuthMessage): ByteArray {
+ val encodedMessage = message.encoded() + eip8padding()
+ val encrypted = cryptoUtils.eciesEncrypt(remotePublicKeyPoint, encodedMessage)
+ return encrypted.encoded()
+ }
+
+ private fun eip8padding(): ByteArray {
+ return randomUtils.randomBytes(200..300)
+ }
+}
\ No newline at end of file
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/connection/Frame.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/connection/Frame.kt
new file mode 100644
index 00000000..1da24981
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/connection/Frame.kt
@@ -0,0 +1,24 @@
+package io.horizontalsystems.ethereumkit.light.net.connection
+
+import io.horizontalsystems.ethereumkit.core.toHexString
+
+class Frame(var type: Int, var payload: ByteArray) {
+ var size: Int = 0
+
+ var totalFrameSize = -1
+ var contextId = -1
+
+ constructor(type: Int, payload: ByteArray, totalFrameSize: Int, contextId: Int) : this(type, payload) {
+ this.totalFrameSize = totalFrameSize
+ this.contextId = contextId
+ }
+
+ init {
+ this.size = payload.size
+ }
+
+ override fun toString(): String {
+ return "Frame [type: $type; size: $size; payload: ${payload.toHexString()}; " +
+ "totalFrameSize: $totalFrameSize; contextId: $contextId]"
+ }
+}
\ No newline at end of file
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/connection/FrameCodec.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/connection/FrameCodec.kt
new file mode 100644
index 00000000..4c2d69e0
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/connection/FrameCodec.kt
@@ -0,0 +1,124 @@
+package io.horizontalsystems.ethereumkit.light.net.connection
+
+import io.horizontalsystems.ethereumkit.light.crypto.CryptoUtils
+import io.horizontalsystems.ethereumkit.light.rlp.RLP
+import io.horizontalsystems.ethereumkit.light.rlp.RLP.rlpDecodeInt
+import io.horizontalsystems.ethereumkit.light.rlp.RLPList
+import org.spongycastle.crypto.digests.KeccakDigest
+import java.io.InputStream
+import java.io.OutputStream
+import java.util.*
+
+class FrameCodec(private val secrets: Secrets,
+ private val frameCodecHelper: FrameCodecHelper = FrameCodecHelper(CryptoUtils),
+ private val enc: AESCipher = AESCipher(secrets.aes, true),
+ private val dec: AESCipher = AESCipher(secrets.aes, false)
+) {
+
+ fun readFrame(inputStream: InputStream): Frame? {
+ val headBuffer = ByteArray(32)
+ inputStream.read(headBuffer)
+
+ val header = headBuffer.copyOfRange(0, 16)
+ val headerMac = headBuffer.copyOfRange(16, 32)
+ val updatedMac = frameCodecHelper.updateMac(secrets.ingressMac, secrets.mac, header)
+
+ if (!updatedMac.contentEquals(headerMac)) {
+ throw FrameCodecError.HeaderMacMismatch()
+ }
+
+ val decryptedHeader = dec.process(header)
+
+ val totalBodySize = frameCodecHelper.fromThreeBytes(decryptedHeader.copyOfRange(0, 3))
+ val rlpList = RLP.decode2OneItem(decryptedHeader, 3) as RLPList
+
+ val protocol = RLP.rlpDecodeInt(rlpList[0])
+ var contextId = -1
+ var totalFrameSize = -1
+
+ if (rlpList.size > 1) {
+ contextId = rlpDecodeInt(rlpList[1])
+ if (rlpList.size > 2) {
+ totalFrameSize = rlpDecodeInt(rlpList[2])
+ }
+ if (contextId > 0) {
+ println("+++++++ Multi-frame message received $contextId $totalFrameSize")
+ }
+ }
+
+ var paddingSize = 16 - totalBodySize % 16
+ if (paddingSize == 16)
+ paddingSize = 0
+ val macSize = 16
+
+ val buffer = ByteArray(totalBodySize + paddingSize + macSize) // body || padding || body-mac
+
+ inputStream.read(buffer)
+
+ val frameSize = buffer.size - macSize
+
+ val frameBodyData = buffer.copyOfRange(0, frameSize)
+ val frameBodyMac = buffer.copyOfRange(frameSize, buffer.size)
+
+ secrets.ingressMac.update(frameBodyData, 0, frameSize)
+ val decryptedFrame = dec.process(frameBodyData)
+
+ var pos = 0
+ val type = RLP.decodeLong(decryptedFrame, pos)
+ pos = RLP.getNextElementIndex(decryptedFrame, pos)
+ val payload = decryptedFrame.copyOfRange(pos, totalBodySize)
+
+ val ingressMac = ByteArray(secrets.ingressMac.digestSize)
+ KeccakDigest(secrets.ingressMac).doFinal(ingressMac, 0)
+
+ val updatedFrameBodyMac = frameCodecHelper.updateMac(secrets.ingressMac, secrets.mac, ingressMac)
+
+ if (!updatedFrameBodyMac.contentEquals(frameBodyMac)) {
+ throw FrameCodecError.BodyMacMismatch()
+ }
+
+ return Frame(type.toInt(), payload, totalFrameSize, contextId)
+ }
+
+ fun writeFrame(frame: Frame, outputStream: OutputStream) {
+ val headBuffer = ByteArray(16)
+ val packetType = RLP.encodeInt(frame.type)
+ val frameSize = frame.size + packetType.size
+
+ System.arraycopy(frameCodecHelper.toThreeBytes(frameSize), 0, headBuffer, 0, 3)
+
+ val headerDataElements = ArrayList()
+ headerDataElements.add(RLP.encodeInt(0))
+ if (frame.contextId >= 0)
+ headerDataElements.add(RLP.encodeInt(frame.contextId))
+ if (frame.totalFrameSize >= 0)
+ headerDataElements.add(RLP.encodeInt(frame.totalFrameSize))
+
+ val headerData = RLP.encodeList(*headerDataElements.toTypedArray())
+ System.arraycopy(headerData, 0, headBuffer, 3, headerData.size)
+
+ val encryptedHeader = enc.process(headBuffer)
+
+ val headerMac = frameCodecHelper.updateMac(secrets.egressMac, secrets.mac, encryptedHeader)
+
+ var frameData = packetType + frame.payload
+ if (frameSize % 16 > 0) {
+ frameData += ByteArray(16 - frameSize % 16)
+ }
+
+ val encryptedFrameData = enc.process(frameData)
+ secrets.egressMac.update(encryptedFrameData, 0, encryptedFrameData.size)
+
+ val egressMac = ByteArray(secrets.egressMac.digestSize)
+ KeccakDigest(secrets.egressMac).doFinal(egressMac, 0)
+
+ val frameMac = frameCodecHelper.updateMac(secrets.egressMac, secrets.mac, egressMac)
+
+ outputStream.write(encryptedHeader + headerMac + encryptedFrameData + frameMac)
+ }
+
+ open class FrameCodecError : Exception() {
+ class HeaderMacMismatch : FrameCodecError()
+ class BodyMacMismatch : FrameCodecError()
+ }
+}
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/connection/FrameCodecHelper.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/connection/FrameCodecHelper.kt
new file mode 100644
index 00000000..01c6b3f7
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/connection/FrameCodecHelper.kt
@@ -0,0 +1,35 @@
+package io.horizontalsystems.ethereumkit.light.net.connection
+
+import io.horizontalsystems.ethereumkit.light.crypto.CryptoUtils
+import io.horizontalsystems.ethereumkit.light.xor
+import org.spongycastle.crypto.digests.KeccakDigest
+
+class FrameCodecHelper(val cryptoUtils: CryptoUtils) {
+
+ fun updateMac(mac: KeccakDigest, macKey: ByteArray, data: ByteArray): ByteArray {
+ val macDigest = ByteArray(mac.digestSize)
+ KeccakDigest(mac).doFinal(macDigest, 0)
+
+ val encryptedMacDigest = cryptoUtils.encryptAES(macKey, macDigest)
+
+ mac.update(encryptedMacDigest.xor(data), 0, 16)
+ KeccakDigest(mac).doFinal(macDigest, 0)
+
+ return macDigest.copyOfRange(0, 16) //checksum
+ }
+
+ fun fromThreeBytes(byteArray: ByteArray): Int {
+ var num = byteArray[0].toInt() and 0xFF
+ num = (num shl 8) + (byteArray[1].toInt() and 0xFF)
+ num = (num shl 8) + (byteArray[2].toInt() and 0xFF)
+ return num
+ }
+
+ fun toThreeBytes(num: Int): ByteArray {
+ val byteArray = ByteArray(3)
+ byteArray[0] = (num shr 16).toByte()
+ byteArray[1] = (num shr 8).toByte()
+ byteArray[2] = num.toByte()
+ return byteArray
+ }
+}
\ No newline at end of file
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/connection/FrameHandler.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/connection/FrameHandler.kt
new file mode 100644
index 00000000..ce3a5630
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/connection/FrameHandler.kt
@@ -0,0 +1,95 @@
+package io.horizontalsystems.ethereumkit.light.net.connection
+
+import io.horizontalsystems.ethereumkit.light.net.ILESMessage
+import io.horizontalsystems.ethereumkit.light.net.IMessage
+import io.horizontalsystems.ethereumkit.light.net.IP2PMessage
+import io.horizontalsystems.ethereumkit.light.net.devp2p.Capability
+import io.horizontalsystems.ethereumkit.light.net.devp2p.messages.DisconnectMessage
+import io.horizontalsystems.ethereumkit.light.net.devp2p.messages.HelloMessage
+import io.horizontalsystems.ethereumkit.light.net.devp2p.messages.PingMessage
+import io.horizontalsystems.ethereumkit.light.net.devp2p.messages.PongMessage
+import io.horizontalsystems.ethereumkit.light.net.les.messages.BlockHeadersMessage
+import io.horizontalsystems.ethereumkit.light.net.les.messages.ProofsMessage
+import io.horizontalsystems.ethereumkit.light.net.les.messages.StatusMessage
+
+class FrameHandler {
+
+ companion object {
+ const val P2P_MAX_MESSAGE_CODE = 0x0F
+ const val LES_MAX_MESSAGE_CODE = 0x15 /*TxStatus*/
+ }
+
+ private val offsets: MutableMap = hashMapOf()
+ private var frames: MutableList = mutableListOf()
+ private val capabilities: MutableList = mutableListOf()
+
+ fun addCapabilities(caps: List) {
+ capabilities.addAll(caps)
+
+ var offset = P2P_MAX_MESSAGE_CODE + 1
+ capabilities.sortedBy { it.name }.forEach { cap ->
+ if (cap.name == Capability.LES) {
+ offsets[Capability.LES] = offset
+ offset += LES_MAX_MESSAGE_CODE + 1
+ }
+ }
+ }
+
+ fun addFrame(frame: Frame) {
+ frames.add(frame)
+ }
+
+ fun getMessage(): IMessage? {
+ val frame = frames.firstOrNull() ?: return null
+
+ frames.remove(frame)
+
+ var message: IMessage? = null
+
+ try {
+ if (frame.type in 0..P2P_MAX_MESSAGE_CODE) {
+ message = when (frame.type) {
+ HelloMessage.code -> HelloMessage(frame.payload)
+ DisconnectMessage.code -> DisconnectMessage(frame.payload)
+ PingMessage.code -> PingMessage()
+ PongMessage.code -> PongMessage()
+ else -> null
+ }
+ }
+
+ val lesOffset = offsets[Capability.LES]
+ if (lesOffset != null && frame.type - lesOffset in 0..LES_MAX_MESSAGE_CODE) {
+ message = when (frame.type - lesOffset) {
+ StatusMessage.code -> StatusMessage(frame.payload)
+ BlockHeadersMessage.code -> BlockHeadersMessage(frame.payload)
+ ProofsMessage.code -> ProofsMessage(frame.payload)
+ else -> null
+ }
+ }
+
+ } catch (ex: Exception) {
+ throw FrameHandlerError.InvalidPayload()
+ }
+
+ return message ?: throw FrameHandlerError.UnknownMessageType()
+ }
+
+ fun getFrames(message: IMessage): List {
+ val frames: MutableList = mutableListOf()
+
+ when (message) {
+ is IP2PMessage -> frames.add(Frame(message.code, message.encoded()))
+ is ILESMessage -> {
+ val frameType = message.code + (offsets[Capability.LES] ?: 0)
+ frames.add(Frame(frameType, message.encoded()))
+ }
+ }
+
+ return frames
+ }
+
+ open class FrameHandlerError : Exception() {
+ class UnknownMessageType : FrameHandlerError()
+ class InvalidPayload : FrameHandlerError()
+ }
+}
\ No newline at end of file
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/connection/Secrets.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/connection/Secrets.kt
new file mode 100644
index 00000000..7e8dc2d7
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/connection/Secrets.kt
@@ -0,0 +1,9 @@
+package io.horizontalsystems.ethereumkit.light.net.connection
+
+import org.spongycastle.crypto.digests.KeccakDigest
+
+data class Secrets(var aes: ByteArray,
+ var mac: ByteArray,
+ var token: ByteArray,
+ var egressMac: KeccakDigest,
+ var ingressMac: KeccakDigest)
\ No newline at end of file
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/connection/messages/AuthAckMessage.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/connection/messages/AuthAckMessage.kt
new file mode 100644
index 00000000..0e7b301b
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/connection/messages/AuthAckMessage.kt
@@ -0,0 +1,33 @@
+package io.horizontalsystems.ethereumkit.light.net.connection.messages
+
+import io.horizontalsystems.ethereumkit.core.toHexString
+import io.horizontalsystems.ethereumkit.light.crypto.CryptoUtils.CURVE
+import io.horizontalsystems.ethereumkit.light.rlp.RLP
+import io.horizontalsystems.ethereumkit.light.rlp.RLPList
+import io.horizontalsystems.ethereumkit.light.toInt
+import org.spongycastle.math.ec.ECPoint
+
+class AuthAckMessage(payload: ByteArray) {
+ val ephemPublicKeyPoint: ECPoint
+ val nonce: ByteArray
+ val version: Int
+
+ init {
+ val params = RLP.decode2OneItem(payload, 0) as RLPList
+ val pubKeyBytes = params[0].rlpData ?: ByteArray(65) { 0 }
+ val bytes = ByteArray(65)
+ System.arraycopy(pubKeyBytes, 0, bytes, 1, 64)
+ bytes[0] = 0x04
+ val ephemeralPublicKey = CURVE.curve.decodePoint(bytes)
+ val nonce = params[1].rlpData
+ val versionBytes = params[2].rlpData
+ val version = versionBytes.toInt()
+ this.ephemPublicKeyPoint = ephemeralPublicKey
+ this.nonce = nonce ?: ByteArray(0)
+ this.version = version
+ }
+
+ override fun toString(): String {
+ return "AuthAckMessage [ephemPublicKeyPoint: $ephemPublicKeyPoint; nonce: ${nonce.toHexString()}; version: $version]"
+ }
+}
\ No newline at end of file
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/connection/messages/AuthMessage.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/connection/messages/AuthMessage.kt
new file mode 100644
index 00000000..306c11da
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/connection/messages/AuthMessage.kt
@@ -0,0 +1,23 @@
+package io.horizontalsystems.ethereumkit.light.net.connection.messages
+
+import io.horizontalsystems.ethereumkit.core.toHexString
+import io.horizontalsystems.ethereumkit.light.rlp.RLP
+import org.spongycastle.math.ec.ECPoint
+
+class AuthMessage(val signature: ByteArray, val publicKeyPoint: ECPoint, val nonce: ByteArray) {
+
+ fun encoded(): ByteArray {
+ val publicKey = ByteArray(64)
+ System.arraycopy(publicKeyPoint.getEncoded(false), 1, publicKey, 0, publicKey.size)
+ val sigBytes = RLP.encode(signature)
+ val publicBytes = RLP.encode(publicKey)
+ val nonceBytes = RLP.encode(nonce)
+ val versionBytes = RLP.encodeInt(4)
+
+ return RLP.encodeList(sigBytes, publicBytes, nonceBytes, versionBytes)
+ }
+
+ override fun toString(): String {
+ return "AuthMessage [signature: ${signature.toHexString()}; ephemPublicKeyPoint: $publicKeyPoint; nonce: ${nonce.toHexString()}]"
+ }
+}
\ No newline at end of file
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/devp2p/Capability.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/devp2p/Capability.kt
new file mode 100644
index 00000000..f2d4834d
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/devp2p/Capability.kt
@@ -0,0 +1,35 @@
+package io.horizontalsystems.ethereumkit.light.net.devp2p
+
+
+data class Capability(val name: String, val version: Byte) : Comparable {
+
+ companion object {
+ val LES = "les"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is Capability) return false
+
+ return this.name == other.name && this.version == other.version
+ }
+
+ override fun compareTo(other: Capability): Int {
+ val cmp = name.compareTo(other.name)
+ return if (cmp != 0) {
+ cmp
+ } else {
+ version.compareTo(other.version)
+ }
+ }
+
+ override fun hashCode(): Int {
+ var result = name.hashCode()
+ result = 31 * result + version.toInt()
+ return result
+ }
+
+ override fun toString(): String {
+ return "$name:$version"
+ }
+}
\ No newline at end of file
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/devp2p/DevP2PPeer.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/devp2p/DevP2PPeer.kt
new file mode 100644
index 00000000..ae291436
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/devp2p/DevP2PPeer.kt
@@ -0,0 +1,108 @@
+package io.horizontalsystems.ethereumkit.light.net.devp2p
+
+import io.horizontalsystems.ethereumkit.light.crypto.ECKey
+import io.horizontalsystems.ethereumkit.light.net.IMessage
+import io.horizontalsystems.ethereumkit.light.net.Node
+import io.horizontalsystems.ethereumkit.light.net.connection.Connection
+import io.horizontalsystems.ethereumkit.light.net.connection.IPeerConnection
+import io.horizontalsystems.ethereumkit.light.net.connection.IPeerConnectionListener
+import io.horizontalsystems.ethereumkit.light.net.devp2p.messages.DisconnectMessage
+import io.horizontalsystems.ethereumkit.light.net.devp2p.messages.HelloMessage
+import io.horizontalsystems.ethereumkit.light.net.devp2p.messages.PingMessage
+import io.horizontalsystems.ethereumkit.light.net.devp2p.messages.PongMessage
+import java.util.concurrent.Executors
+
+interface IDevP2PPeerListener {
+ fun onConnectionEstablished()
+ fun onDisconnected(error: Throwable?)
+ fun onMessageReceived(message: IMessage)
+}
+
+class DevP2PPeer(val key: ECKey, val node: Node, val capability: Capability, val listener: IDevP2PPeerListener) : IPeerConnectionListener {
+
+ private var connection: IPeerConnection = Connection(node, this)
+ private val executor = Executors.newSingleThreadExecutor()
+
+ var helloSent = false
+ var helloReceived = false
+
+ private fun proceedHandshake() {
+ if (helloSent) {
+ if (helloReceived) {
+ connection.register(listOf(capability))
+ listener.onConnectionEstablished()
+ return
+ }
+ } else {
+ var myNodeId = key.publicKeyPoint.getEncoded(false)
+ myNodeId = myNodeId.copyOfRange(1, myNodeId.size)
+ val helloMessage = HelloMessage(myNodeId, 30303, listOf(capability))
+ connection.send(helloMessage)
+ helloSent = true
+ }
+ }
+
+ private fun handle(message: IMessage) {
+ println("<<<<<<< $message \n")
+ when (message) {
+ is HelloMessage -> handle(message)
+ is DisconnectMessage -> handle(message)
+ is PingMessage -> handle(message)
+ is PongMessage -> handle(message)
+ else -> listener.onMessageReceived(message)
+ }
+ }
+
+ private fun handle(message: HelloMessage) {
+ helloReceived = true
+ proceedHandshake()
+ }
+
+ private fun handle(message: DisconnectMessage) {
+ }
+
+ private fun handle(message: PingMessage) {
+ connection.send(PongMessage())
+ }
+
+ private fun handle(message: PongMessage) {
+ }
+
+ //------------------Public methods----------------------
+
+ fun connect() {
+ connection.connect()
+ }
+
+ fun disconnect(error: Throwable?) {
+ connection.disconnect(error)
+ }
+
+ fun send(message: IMessage) {
+ connection.send(message)
+ }
+
+
+ //-----------IPeerConnectionListener methods------------
+
+ override fun connectionKey(): ECKey {
+ return key
+ }
+
+ override fun onConnectionEstablished() {
+ println("DevP2PPeer -> onConnectionEstablished \n")
+ proceedHandshake()
+ }
+
+ override fun onDisconnected(error: Throwable?) {
+ println("DevP2PPeer -> onDisconnected")
+ }
+
+ override fun onMessageReceived(message: IMessage) {
+ executor.execute {
+ handle(message)
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/devp2p/messages/DisconnectMessage.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/devp2p/messages/DisconnectMessage.kt
new file mode 100644
index 00000000..373ed4d3
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/devp2p/messages/DisconnectMessage.kt
@@ -0,0 +1,41 @@
+package io.horizontalsystems.ethereumkit.light.net.devp2p.messages
+
+import io.horizontalsystems.ethereumkit.light.net.IP2PMessage
+import io.horizontalsystems.ethereumkit.light.rlp.RLP
+import io.horizontalsystems.ethereumkit.light.rlp.RLPList
+
+class DisconnectMessage : IP2PMessage {
+
+ private var reason: ReasonCode = ReasonCode.UNKNOWN
+ override var code: Int = Companion.code
+
+ companion object {
+ const val code = 0x01
+ }
+
+ override fun encoded(): ByteArray {
+ val encodedReason = RLP.encodeByte(this.reason.asByte())
+ return RLP.encodeList(encodedReason)
+ }
+
+ constructor(reason: ReasonCode) {
+ this.reason = reason
+ }
+
+ constructor(payload: ByteArray) {
+ val paramsList = RLP.decode2(payload)[0] as RLPList
+ reason = if (paramsList.size > 0) {
+ val reasonBytes = paramsList[0].rlpData
+ if (reasonBytes == null)
+ ReasonCode.UNKNOWN
+ else
+ ReasonCode.fromInt(reasonBytes[0].toInt())
+ } else {
+ ReasonCode.UNKNOWN
+ }
+ }
+
+ override fun toString(): String {
+ return "DisconnectMessage [reason: ${reason.name}; code: ${reason.code}]"
+ }
+}
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/devp2p/messages/HelloMessage.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/devp2p/messages/HelloMessage.kt
new file mode 100644
index 00000000..997d14ff
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/devp2p/messages/HelloMessage.kt
@@ -0,0 +1,85 @@
+package io.horizontalsystems.ethereumkit.light.net.devp2p.messages
+
+import io.horizontalsystems.ethereumkit.core.toHexString
+import io.horizontalsystems.ethereumkit.light.net.IP2PMessage
+import io.horizontalsystems.ethereumkit.light.net.devp2p.Capability
+import io.horizontalsystems.ethereumkit.light.rlp.RLP
+import io.horizontalsystems.ethereumkit.light.rlp.RLPList
+import io.horizontalsystems.ethereumkit.light.toInt
+import java.util.*
+
+class HelloMessage : IP2PMessage {
+
+ companion object {
+ const val code = 0x00
+ }
+
+ private var peerId: ByteArray = byteArrayOf()
+ private var port: Int = 0
+ private var p2pVersion: Byte = 4
+ private var clientId: String = "EthereumKit"
+ private var capabilities: List
+
+ constructor(peerId: ByteArray, port: Int, capabilities: List) {
+ this.peerId = peerId
+ this.port = port
+ this.capabilities = capabilities
+ }
+
+ constructor(payload: ByteArray) {
+ val paramsList = RLP.decode2(payload)[0] as RLPList
+
+ val p2pVersionBytes = paramsList[0].rlpData
+ p2pVersion = p2pVersionBytes?.get(0) ?: 0
+
+ clientId = String(paramsList[1].rlpData ?: byteArrayOf())
+
+ val capabilityList = paramsList[2] as RLPList
+ val caps = ArrayList()
+ for (aCapabilityList in capabilityList) {
+
+ val capId = (aCapabilityList as RLPList)[0]
+ val capVersion = (aCapabilityList)[1]
+
+ val name = String(capId.rlpData ?: byteArrayOf())
+
+ val version = (capVersion.rlpData?.get(0) ?: 0).toByte()
+
+ val cap = Capability(name, version)
+ caps.add(cap)
+ }
+
+ capabilities = caps
+
+ val peerPortBytes = paramsList[3].rlpData
+ port = peerPortBytes.toInt()
+
+ peerId = paramsList[4].rlpData ?: byteArrayOf()
+ }
+
+ override var code: Int = HelloMessage.code
+
+ override fun encoded(): ByteArray {
+ val p2pVersion = RLP.encodeByte(this.p2pVersion)
+ val clientId = RLP.encodeString(this.clientId)
+ val capabilities = arrayOfNulls(this.capabilities.size)
+ for (i in this.capabilities.indices) {
+ val capability = this.capabilities[i]
+ capabilities[i] = RLP.encodeList(
+ RLP.encodeElement(capability.name.toByteArray()),
+ RLP.encodeInt(capability.version.toInt()))
+ }
+ val capabilityList = RLP.encodeList(*capabilities.mapNotNull { it }.toTypedArray())
+
+ val peerPort = RLP.encodeInt(this.port)
+ val peerId = RLP.encodeElement(this.peerId)
+
+ return RLP.encodeList(p2pVersion, clientId, capabilityList, peerPort, peerId)
+ }
+
+ override fun toString(): String {
+ return "Hello [version: $p2pVersion; clientId: $clientId; " +
+ "capabilities: ${capabilities.joinToString { "${it.name}/${it.version}" }}; " +
+ "peerId: ${peerId.toHexString()}; port: $port]"
+ }
+}
\ No newline at end of file
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/devp2p/messages/PingMessage.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/devp2p/messages/PingMessage.kt
new file mode 100644
index 00000000..8d68b2ea
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/devp2p/messages/PingMessage.kt
@@ -0,0 +1,21 @@
+package io.horizontalsystems.ethereumkit.light.net.devp2p.messages
+
+import io.horizontalsystems.ethereumkit.core.hexStringToByteArray
+import io.horizontalsystems.ethereumkit.light.net.IP2PMessage
+
+class PingMessage : IP2PMessage {
+ override var code = PingMessage.code
+
+ override fun encoded(): ByteArray {
+ return payload
+ }
+
+ override fun toString(): String {
+ return "Ping"
+ }
+
+ companion object {
+ const val code = 0x02
+ val payload = "C0".hexStringToByteArray()
+ }
+}
\ No newline at end of file
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/devp2p/messages/PongMessage.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/devp2p/messages/PongMessage.kt
new file mode 100644
index 00000000..023cd307
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/devp2p/messages/PongMessage.kt
@@ -0,0 +1,21 @@
+package io.horizontalsystems.ethereumkit.light.net.devp2p.messages
+
+import io.horizontalsystems.ethereumkit.core.hexStringToByteArray
+import io.horizontalsystems.ethereumkit.light.net.IP2PMessage
+
+class PongMessage : IP2PMessage {
+ override var code = PongMessage.code
+
+ override fun encoded(): ByteArray {
+ return payload
+ }
+
+ override fun toString(): String {
+ return "Pong"
+ }
+
+ companion object {
+ const val code = 0x03
+ val payload = "C0".hexStringToByteArray()
+ }
+}
\ No newline at end of file
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/devp2p/messages/ReasonCode.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/devp2p/messages/ReasonCode.kt
new file mode 100644
index 00000000..3d261892
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/devp2p/messages/ReasonCode.kt
@@ -0,0 +1,37 @@
+package io.horizontalsystems.ethereumkit.light.net.devp2p.messages
+
+enum class ReasonCode(val code: Int) {
+
+ REQUESTED(0x00),
+ TCP_ERROR(0x01),
+ BAD_PROTOCOL(0x02),
+ USELESS_PEER(0x03),
+ TOO_MANY_PEERS(0x04),
+ DUPLICATE_PEER(0x05),
+ INCOMPATIBLE_PROTOCOL(0x06),
+ NULL_IDENTITY(0x07),
+ PEER_QUITING(0x08),
+ UNEXPECTED_IDENTITY(0x09),
+ LOCAL_IDENTITY(0x0A),
+ PING_TIMEOUT(0x0B),
+ USER_REASON(0x10),
+ UNKNOWN(0xFF);
+
+ fun asByte(): Byte {
+ return code.toByte()
+ }
+
+ companion object {
+ private val intToTypeMap: MutableMap = hashMapOf()
+
+ init {
+ for (type in ReasonCode.values()) {
+ intToTypeMap[type.code] = type
+ }
+ }
+
+ fun fromInt(i: Int): ReasonCode {
+ return intToTypeMap[i] ?: return UNKNOWN
+ }
+ }
+}
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/les/LESPeer.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/les/LESPeer.kt
new file mode 100644
index 00000000..b6efb16f
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/les/LESPeer.kt
@@ -0,0 +1,110 @@
+package io.horizontalsystems.ethereumkit.light.net.les
+
+import io.horizontalsystems.ethereumkit.light.crypto.ECKey
+import io.horizontalsystems.ethereumkit.light.models.BlockHeader
+import io.horizontalsystems.ethereumkit.light.net.IMessage
+import io.horizontalsystems.ethereumkit.light.net.INetwork
+import io.horizontalsystems.ethereumkit.light.net.Node
+import io.horizontalsystems.ethereumkit.light.net.devp2p.Capability
+import io.horizontalsystems.ethereumkit.light.net.devp2p.DevP2PPeer
+import io.horizontalsystems.ethereumkit.light.net.devp2p.IDevP2PPeerListener
+import io.horizontalsystems.ethereumkit.light.net.les.messages.*
+import java.util.*
+
+
+interface IPeerListener {
+ fun connected()
+ fun blocksReceived(blockHeaders: List)
+ fun proofReceived(message: ProofsMessage)
+}
+
+class Peer(val network: INetwork, val bestBlock: BlockHeader, key: ECKey, val node: Node, val listener: IPeerListener) : IDevP2PPeerListener {
+
+ private val protocolVersion: Byte = 2
+ private var devP2PPeer: DevP2PPeer = DevP2PPeer(key, node, Capability( "les", 2), this)
+
+
+ var statusSent = false
+ var statusReceived = false
+
+ private fun proceedHandshake() {
+ if (statusSent) {
+ if (statusReceived) {
+ listener.connected()
+ return
+ }
+ } else {
+ val statusMessage = StatusMessage(
+ protocolVersion = protocolVersion,
+ networkId = network.id,
+ genesisHash = network.genesisBlockHash,
+ bestBlockTotalDifficulty = bestBlock.totalDifficulty,
+ bestBlockHash = bestBlock.hashHex,
+ bestBlockHeight = bestBlock.height
+ )
+
+ devP2PPeer.send(statusMessage)
+ statusSent = true
+ }
+ }
+
+ private fun handle(message: IMessage) {
+ when (message) {
+ is StatusMessage -> handle(message)
+ is BlockHeadersMessage -> handle(message)
+ is ProofsMessage -> handle(message)
+ }
+ }
+
+ private fun handle(message: StatusMessage) {
+ statusReceived = true
+
+ proceedHandshake()
+ }
+
+ private fun handle(message: BlockHeadersMessage) {
+ listener.blocksReceived(message.headers.drop(1))
+ }
+
+ private fun handle(message: ProofsMessage) {
+ listener.proofReceived(message)
+ }
+
+ //------------------Public methods----------------------
+
+ fun connect() {
+ devP2PPeer.connect()
+ }
+
+ fun disconnect(error: Throwable?) {
+ devP2PPeer.disconnect(error)
+ }
+
+ fun downloadBlocksFrom(block: BlockHeader) {
+ val message = GetBlockHeadersMessage(requestID = Math.abs(Random().nextLong()), blockHash = block.hashHex)
+
+ devP2PPeer.send(message)
+ }
+
+ fun getBalance(address: ByteArray, blockHash: ByteArray) {
+ val message = GetProofsMessage(requestID = Math.abs(Random().nextLong()), blockHash = blockHash, key = address, key2 = ByteArray(0))
+
+ devP2PPeer.send(message)
+ }
+
+ //-----------IDevP2PPeerListener methods------------
+
+ override fun onConnectionEstablished() {
+ println("Peer -> onConnectionEstablished\n")
+ proceedHandshake()
+ }
+
+ override fun onDisconnected(error: Throwable?) {
+ println("Peer -> onDisconnected")
+ }
+
+ override fun onMessageReceived(message: IMessage) {
+ println("Peer -> onMessageReceived\n")
+ handle(message)
+ }
+}
\ No newline at end of file
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/les/TrieNode.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/les/TrieNode.kt
new file mode 100644
index 00000000..cfd22002
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/les/TrieNode.kt
@@ -0,0 +1,91 @@
+package io.horizontalsystems.ethereumkit.light.net.les
+
+import io.horizontalsystems.ethereumkit.core.toHexString
+import io.horizontalsystems.ethereumkit.light.crypto.CryptoUtils
+import io.horizontalsystems.ethereumkit.light.rlp.RLPList
+import java.util.*
+
+class TrieNode(rlpList: RLPList) {
+
+ companion object {
+ private val alphabet = charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f')
+ }
+
+ enum class NodeType {
+ NULL,
+ BRANCH,
+ EXTENSION,
+ LEAF
+ }
+
+ var nodeType: NodeType = NodeType.NULL
+
+ val elements: MutableList
+ var encodedPath: String? = null
+ var hash: ByteArray
+
+ init {
+ this.elements = ArrayList()
+ for (element in rlpList) {
+ this.elements.add(element.rlpData ?: ByteArray(0))
+ }
+
+ this.hash = CryptoUtils.sha3(rlpList.rlpData ?: ByteArray(0))
+
+ if (rlpList.size == 17) {
+ this.nodeType = NodeType.BRANCH
+ } else {
+ val first = this.elements[0]
+ val nibble = ((first[0].toInt() and 0xFF) shr 4).toByte()
+
+ when (nibble.toInt()) {
+ 0 -> {
+ this.nodeType = NodeType.EXTENSION
+ encodedPath = Arrays.copyOfRange(first, 1, first.size).toHexString()
+ }
+
+ 1 -> {
+ this.nodeType = NodeType.EXTENSION
+ encodedPath = Arrays.copyOfRange(first, 1, first.size).toHexString()
+ val firstByte = ((((first[0].toInt() and 0xFF) shl 4) and 0xFF) shr 4).toByte()
+ val firstByteString = byteArrayOf(firstByte).toHexString()
+ encodedPath = firstByteString.substring(1) + encodedPath
+ }
+
+ 2 -> {
+ this.nodeType = NodeType.LEAF
+ encodedPath = Arrays.copyOfRange(first, 1, first.size).toHexString()
+ }
+
+ 3 -> {
+ this.nodeType = NodeType.LEAF
+ encodedPath = Arrays.copyOfRange(first, 1, first.size).toHexString()
+ val firstByte = ((((first[0].toInt() and 0xFF) shl 4) and 0xFF) shr 4).toByte()
+ val firstByteString = byteArrayOf(firstByte).toHexString()
+ encodedPath = firstByteString.substring(1) + encodedPath
+ }
+ }
+ }
+ }
+
+ fun getPath(element: ByteArray?): String? {
+ if (element == null && nodeType == NodeType.LEAF) {
+ return encodedPath
+ }
+
+ for (i in elements.indices) {
+ if (Arrays.equals(elements[i], element)) {
+ if (nodeType == NodeType.BRANCH) {
+ return alphabet[i].toString()
+ } else if (nodeType == NodeType.EXTENSION) {
+ return encodedPath
+ }
+ }
+ }
+ return null
+ }
+
+ override fun toString(): String {
+ return "(${elements.map { it.toHexString() }.joinToString(separator = " | ")})"
+ }
+}
\ No newline at end of file
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/les/messages/BlockHeadersMessage.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/les/messages/BlockHeadersMessage.kt
new file mode 100644
index 00000000..4a4b52b2
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/les/messages/BlockHeadersMessage.kt
@@ -0,0 +1,39 @@
+package io.horizontalsystems.ethereumkit.light.net.les.messages
+
+import io.horizontalsystems.ethereumkit.light.models.BlockHeader
+import io.horizontalsystems.ethereumkit.light.net.ILESMessage
+import io.horizontalsystems.ethereumkit.light.rlp.RLP
+import io.horizontalsystems.ethereumkit.light.rlp.RLPList
+import io.horizontalsystems.ethereumkit.light.toLong
+
+class BlockHeadersMessage(payload: ByteArray) : ILESMessage {
+
+ companion object {
+ const val code = 0x03
+ }
+
+ var requestID: Long = 0
+ var bv: Long = 0
+ var headers: MutableList = mutableListOf()
+
+ init {
+ val paramsList = RLP.decode2(payload)[0] as RLPList
+ this.requestID = paramsList[0].rlpData.toLong()
+ this.bv = paramsList[1].rlpData.toLong()
+ val payloadList = paramsList[2] as RLPList
+ for (i in 0 until payloadList.size) {
+ val rlpData = payloadList[i] as RLPList
+ headers.add(BlockHeader(rlpData))
+ }
+ }
+
+ override var code: Int = Companion.code
+
+ override fun encoded(): ByteArray {
+ return byteArrayOf()
+ }
+
+ override fun toString(): String {
+ return "Headers [requestId: $requestID; bv: $bv; headers (${headers.size}): [${headers.joinToString(separator = ", ")}}] ]"
+ }
+}
\ No newline at end of file
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/les/messages/GetBlockHeadersMessage.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/les/messages/GetBlockHeadersMessage.kt
new file mode 100644
index 00000000..e1163b2a
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/les/messages/GetBlockHeadersMessage.kt
@@ -0,0 +1,35 @@
+package io.horizontalsystems.ethereumkit.light.net.les.messages
+
+import io.horizontalsystems.ethereumkit.core.toHexString
+import io.horizontalsystems.ethereumkit.light.net.ILESMessage
+import io.horizontalsystems.ethereumkit.light.rlp.RLP
+import java.math.BigInteger
+
+class GetBlockHeadersMessage(val requestID: Long,
+ val blockHash: ByteArray,
+ val skip: Int = 0,
+ val reverse: Int = 0) : ILESMessage {
+ companion object {
+ const val code = 0x02
+ const val maxHeaders = 50
+ }
+
+ override var code: Int = Companion.code
+
+ override fun encoded(): ByteArray {
+ val reqID = RLP.encodeBigInteger(BigInteger.valueOf(this.requestID))
+ val maxHeaders = RLP.encodeInt(maxHeaders)
+ val skipBlocks = RLP.encodeInt(skip)
+ val reverse = RLP.encodeByte(reverse.toByte())
+ val hash = RLP.encodeElement(this.blockHash)
+
+ var encoded = RLP.encodeList(hash, maxHeaders, skipBlocks, reverse)
+ encoded = RLP.encodeList(reqID, encoded)
+
+ return encoded
+ }
+
+ override fun toString(): String {
+ return "GetHeaders [requestId: $requestID; blockHash: ${blockHash.toHexString()}; maxHeaders: $maxHeaders; skip: $skip; reverse: $reverse]"
+ }
+}
\ No newline at end of file
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/les/messages/GetProofsMessage.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/les/messages/GetProofsMessage.kt
new file mode 100644
index 00000000..f27b9a56
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/les/messages/GetProofsMessage.kt
@@ -0,0 +1,64 @@
+package io.horizontalsystems.ethereumkit.light.net.les.messages
+
+import io.horizontalsystems.ethereumkit.core.toHexString
+import io.horizontalsystems.ethereumkit.light.crypto.CryptoUtils
+import io.horizontalsystems.ethereumkit.light.net.ILESMessage
+import io.horizontalsystems.ethereumkit.light.rlp.RLP
+import java.math.BigInteger
+
+class GetProofsMessage(requestID: Long, blockHash: ByteArray, key: ByteArray, key2: ByteArray = byteArrayOf(), fromLevel: Int = 0) : ILESMessage {
+
+ companion object {
+ const val code = 0x0F
+ }
+
+ private val requestID: Long = requestID
+ private var proofRequests: List = listOf(ProofRequest(blockHash, key, key2, fromLevel))
+
+ override var code: Int = Companion.code
+
+ override fun encoded(): ByteArray {
+ val reqID = RLP.encodeBigInteger(BigInteger.valueOf(this.requestID))
+
+ val encodedProofs = this.proofRequests.map { it.asRLPEncoded() }
+ val proofsList = RLP.encodeList(*encodedProofs.toTypedArray())
+
+ return RLP.encodeList(reqID, proofsList)
+ }
+
+ override fun toString(): String {
+ return "GetProofs [requestID: $requestID; proofRequests: [${proofRequests.map { it.toString() }.joinToString(separator = ",")}]]"
+ }
+
+ class ProofRequest(blockHash: ByteArray, key: ByteArray, key2: ByteArray, fromLevel: Int) {
+
+ private val blockHash: ByteArray
+ private val keyHash: ByteArray
+ private val key2Hash: ByteArray
+ private val fromLevel: Int
+
+ init {
+ this.blockHash = blockHash
+ this.keyHash = CryptoUtils.sha3(key)
+ if (key2.isNotEmpty()) {
+ this.key2Hash = CryptoUtils.sha3(key2)
+ } else {
+ this.key2Hash = key2
+ }
+ this.fromLevel = fromLevel
+ }
+
+ fun asRLPEncoded(): ByteArray {
+ return RLP.encodeList(
+ RLP.encodeElement(blockHash),
+ RLP.encodeElement(key2Hash),
+ RLP.encodeElement(keyHash),
+ RLP.encodeInt(fromLevel)
+ )
+ }
+
+ override fun toString(): String {
+ return "(blockHash: ${blockHash.toHexString()}; key: ${keyHash.toHexString()}; key2: ${key2Hash.toHexString()}; fromLevel: $fromLevel)"
+ }
+ }
+}
\ No newline at end of file
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/les/messages/ProofsMessage.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/les/messages/ProofsMessage.kt
new file mode 100644
index 00000000..67348b04
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/les/messages/ProofsMessage.kt
@@ -0,0 +1,97 @@
+package io.horizontalsystems.ethereumkit.light.net.les.messages
+
+import io.horizontalsystems.ethereumkit.core.toHexString
+import io.horizontalsystems.ethereumkit.light.crypto.CryptoUtils
+import io.horizontalsystems.ethereumkit.light.models.AccountState
+import io.horizontalsystems.ethereumkit.light.net.ILESMessage
+import io.horizontalsystems.ethereumkit.light.net.les.TrieNode
+import io.horizontalsystems.ethereumkit.light.rlp.RLP
+import io.horizontalsystems.ethereumkit.light.rlp.RLPList
+import io.horizontalsystems.ethereumkit.light.toBigInteger
+import io.horizontalsystems.ethereumkit.light.toLong
+
+class ProofsMessage(data: ByteArray) : ILESMessage {
+
+ companion object {
+ const val code = 0x10
+ }
+
+ var requestID: Long = 0
+ var bv: Long = 0
+ var nodes: MutableList = mutableListOf()
+
+ init {
+
+ val params = RLP.decode2(data)[0] as RLPList
+ this.requestID = params[0].rlpData.toLong()
+ this.bv = params[1].rlpData.toLong()
+ val rlpList = params[2] as RLPList
+ if (rlpList.isNotEmpty()) {
+ for (rlpNode in rlpList) {
+ nodes.add(TrieNode(rlpNode as RLPList))
+ }
+ }
+ }
+
+ fun getValidatedState(stateRoot: ByteArray, address: ByteArray): AccountState {
+
+ var lastNode = nodes.lastOrNull() ?: throw ProofError.NoNodes()
+
+ check(lastNode.nodeType == TrieNode.NodeType.LEAF) {
+ throw ProofError.StateNodeNotFound()
+ }
+
+ var path = lastNode.getPath(null)
+ ?: throw ProofError.StateNodeNotFound()
+
+ val valueRLP = lastNode.elements[1]
+ val value = RLP.decode2(valueRLP)[0] as RLPList
+
+ val nonce = value[0].rlpData.toLong()
+ val balance = value[1].rlpData.toBigInteger()
+ val storageRoot = value[2].rlpData ?: ByteArray(0)
+ val codeHash = value[3].rlpData ?: ByteArray(0)
+
+ var lastNodeKey = lastNode.hash
+
+ for (i in nodes.size - 2 downTo 0) {
+ lastNode = nodes[i]
+ val partialPath = lastNode.getPath(lastNodeKey)
+ ?: throw ProofError.NodesNotInterconnected()
+
+ path = partialPath + path
+
+ lastNodeKey = lastNode.hash
+ }
+
+ val addressHash = CryptoUtils.sha3(address)
+
+ check(addressHash.toHexString() == path) {
+ throw ProofError.PathDoesNotMatchAddressHash()
+ }
+
+ check(stateRoot.contentEquals(lastNodeKey)) {
+ throw ProofError.RootHashDoesNotMatchStateRoot()
+ }
+
+ return AccountState(address, nonce, balance, storageRoot, codeHash)
+ }
+
+ override var code: Int = Companion.code
+
+ override fun encoded(): ByteArray {
+ return ByteArray(0)
+ }
+
+ override fun toString(): String {
+ return "Proofs [requestID: $requestID; bv: $bv; nodes: [${nodes.joinToString(separator = ", ") { it.toString() }}]]"
+ }
+
+ open class ProofError : Exception() {
+ class NoNodes : ProofError()
+ class StateNodeNotFound : ProofError()
+ class NodesNotInterconnected : ProofError()
+ class PathDoesNotMatchAddressHash : ProofError()
+ class RootHashDoesNotMatchStateRoot : ProofError()
+ }
+}
\ No newline at end of file
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/les/messages/StatusMessage.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/les/messages/StatusMessage.kt
new file mode 100644
index 00000000..13341b2c
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/net/les/messages/StatusMessage.kt
@@ -0,0 +1,70 @@
+package io.horizontalsystems.ethereumkit.light.net.les.messages
+
+import io.horizontalsystems.ethereumkit.core.toHexString
+import io.horizontalsystems.ethereumkit.light.net.ILESMessage
+import io.horizontalsystems.ethereumkit.light.rlp.RLP
+import io.horizontalsystems.ethereumkit.light.rlp.RLPList
+import io.horizontalsystems.ethereumkit.light.toBigInteger
+import io.horizontalsystems.ethereumkit.light.toInt
+import java.math.BigInteger
+
+class StatusMessage : ILESMessage {
+
+ companion object {
+ const val code = 0x00
+ }
+
+ private var protocolVersion: Byte = 0
+ private var networkId: Int = 0
+ private var genesisHash: ByteArray = byteArrayOf()
+ private var bestBlockTotalDifficulty: ByteArray = byteArrayOf()
+ private var bestBlockHash: ByteArray = byteArrayOf()
+ private var bestBlockHeight: BigInteger = BigInteger.ZERO
+
+ constructor(protocolVersion: Byte, networkId: Int,
+ genesisHash: ByteArray, bestBlockTotalDifficulty: ByteArray,
+ bestBlockHash: ByteArray, bestBlockHeight: BigInteger) {
+ this.protocolVersion = protocolVersion
+ this.networkId = networkId
+ this.genesisHash = genesisHash
+ this.bestBlockTotalDifficulty = bestBlockTotalDifficulty
+ this.bestBlockHash = bestBlockHash
+ this.bestBlockHeight = bestBlockHeight
+ }
+
+ constructor(payload: ByteArray) {
+ val paramsList = RLP.decode2(payload)[0] as RLPList
+
+ protocolVersion = (paramsList[0] as RLPList)[1].rlpData?.get(0) ?: 0
+ val networkIdBytes = (paramsList[1] as RLPList)[1].rlpData
+
+ networkId = networkIdBytes.toInt()
+ val difficultyBytes = (paramsList[2] as RLPList)[1].rlpData
+
+ bestBlockTotalDifficulty = difficultyBytes ?: byteArrayOf()
+ bestBlockHash = (paramsList[3] as RLPList)[1].rlpData ?: byteArrayOf()
+ bestBlockHeight = (paramsList[4] as RLPList)[1].rlpData.toBigInteger()
+ genesisHash = (paramsList[5] as RLPList)[1].rlpData ?: byteArrayOf()
+ }
+
+ override var code: Int = Companion.code
+
+ override fun encoded(): ByteArray {
+ val protocolVersion = RLP.encodeList(RLP.encodeString("protocolVersion"), RLP.encodeByte(this.protocolVersion))
+ val networkId = RLP.encodeList(RLP.encodeString("networkId"), RLP.encodeInt(this.networkId))
+ val totalDifficulty = RLP.encodeList(RLP.encodeString("headTd"), RLP.encodeElement(this.bestBlockTotalDifficulty))
+ val bestHash = RLP.encodeList(RLP.encodeString("headHash"), RLP.encodeElement(this.bestBlockHash))
+ val bestNum = RLP.encodeList(RLP.encodeString("headNum"), RLP.encodeBigInteger(this.bestBlockHeight))
+ val genesisHash = RLP.encodeList(RLP.encodeString("genesisHash"), RLP.encodeElement(this.genesisHash))
+ val announceType = RLP.encodeList(RLP.encodeString("announceType"), RLP.encodeByte(1.toByte()))
+
+ return RLP.encodeList(protocolVersion, networkId, totalDifficulty, bestHash, bestNum, genesisHash, announceType)
+ }
+
+ override fun toString(): String {
+ return "Status [protocolVersion: $protocolVersion; networkId: $networkId; " +
+ "totalDifficulty: ${bestBlockTotalDifficulty.toHexString()}; " +
+ "bestHash: ${bestBlockHash.toHexString()}; bestNum: $bestBlockHeight; " +
+ "genesisHash: ${genesisHash.toHexString()}]"
+ }
+}
\ No newline at end of file
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/rlp/DecodeResult.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/rlp/DecodeResult.kt
new file mode 100644
index 00000000..b7d05f44
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/rlp/DecodeResult.kt
@@ -0,0 +1,24 @@
+package io.horizontalsystems.ethereumkit.light.rlp
+
+import org.spongycastle.util.encoders.Hex
+import java.io.Serializable
+
+class DecodeResult(val pos: Int, val decoded: Any) : Serializable {
+
+ override fun toString(): String {
+ return asString(this.decoded)
+ }
+
+ private fun asString(decoded: Any?): String = when (decoded) {
+ is String -> decoded
+ is ByteArray -> Hex.toHexString(decoded)
+ is Array<*> -> {
+ val result = StringBuilder()
+ for (item in decoded) {
+ result.append(asString(item))
+ }
+ result.toString()
+ }
+ else -> ""
+ }
+}
\ No newline at end of file
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/rlp/RLP.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/rlp/RLP.kt
new file mode 100644
index 00000000..34089e1b
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/rlp/RLP.kt
@@ -0,0 +1,496 @@
+package io.horizontalsystems.ethereumkit.light.rlp
+
+import io.horizontalsystems.ethereumkit.light.toBytesNoLeadZeroes
+import io.horizontalsystems.ethereumkit.light.toInt
+import org.spongycastle.util.Arrays.concatenate
+import org.spongycastle.util.BigIntegers.asUnsignedByteArray
+import org.spongycastle.util.encoders.Hex
+import java.io.Serializable
+import java.math.BigInteger
+import java.util.*
+import java.util.Arrays.copyOfRange
+import kotlin.experimental.and
+
+interface RLPElement : Serializable {
+ val rlpData: ByteArray?
+}
+
+class RLPItem(initValue: ByteArray?) : RLPElement {
+ override val rlpData: ByteArray? = initValue
+ get() = if (field?.isNotEmpty() == true) field else null
+}
+
+class RLPList : ArrayList(), RLPElement {
+ override var rlpData: ByteArray? = null
+}
+
+object RLP {
+ private const val OFFSET_SHORT_ITEM = 0x80
+ private const val OFFSET_LONG_ITEM = 0xb7
+ private const val OFFSET_SHORT_LIST = 0xc0
+ private const val OFFSET_LONG_LIST = 0xf7
+ private const val SIZE_THRESHOLD = 56
+
+ private const val MAX_DEPTH = 16
+ private val MAX_ITEM_LENGTH = Math.pow(256.0, 8.0)
+
+ fun encode(input: Any): ByteArray {
+ val value = Value(input)
+ if (value.isList()) {
+ val inputArray = value.asList()
+ if (inputArray.isEmpty()) {
+ return encodeLength(inputArray.size, OFFSET_SHORT_LIST)
+ }
+ var output = byteArrayOf()
+ for (any in inputArray) {
+ output = concatenate(output, encode(any))
+ }
+ val prefix = encodeLength(output.size, OFFSET_SHORT_LIST)
+ return concatenate(prefix, output)
+ } else {
+ val inputAsBytes = toBytes(input)
+ if (inputAsBytes.size == 1 && (inputAsBytes[0].toInt() and 0xFF) <= 0x80) {
+ return inputAsBytes
+ } else {
+ val firstByte = encodeLength(inputAsBytes.size, OFFSET_SHORT_ITEM)
+ return concatenate(firstByte, inputAsBytes)
+ }
+ }
+ }
+
+ fun encodeInt(singleInt: Int) = when (singleInt) {
+ singleInt and 0xFF -> encodeByte(singleInt.toByte())
+ singleInt and 0xFFFF -> encodeShort(singleInt.toShort())
+ singleInt and 0xFFFFFF -> byteArrayOf((OFFSET_SHORT_ITEM + 3).toByte(), singleInt.ushr(16).toByte(), singleInt.ushr(8).toByte(), singleInt.toByte())
+ else -> byteArrayOf((OFFSET_SHORT_ITEM + 4).toByte(), singleInt.ushr(24).toByte(), singleInt.ushr(16).toByte(), singleInt.ushr(8).toByte(), singleInt.toByte())
+ }
+
+ fun encodeByte(singleByte: Byte) = when {
+ (singleByte.toInt() and 0xFF) == 0 -> byteArrayOf(OFFSET_SHORT_ITEM.toByte())
+ (singleByte.toInt() and 0xFF) <= 0x7F -> byteArrayOf(singleByte)
+ else -> byteArrayOf((OFFSET_SHORT_ITEM + 1).toByte(), singleByte)
+ }
+
+ fun encodeString(srcString: String): ByteArray {
+ return encodeElement(srcString.toByteArray())
+ }
+
+ fun encodeBigInteger(srcBigInteger: BigInteger): ByteArray {
+ if (srcBigInteger < BigInteger.ZERO)
+ throw RuntimeException("negative numbers are not allowed")
+
+ return if (srcBigInteger == BigInteger.ZERO)
+ encodeByte(0.toByte())
+ else
+ encodeElement(asUnsignedByteArray(srcBigInteger))
+ }
+
+ fun encodeElement(srcData: ByteArray?): ByteArray {
+
+ // [0x80]
+ if (srcData == null || srcData.isEmpty()) {
+ return byteArrayOf(OFFSET_SHORT_ITEM.toByte())
+
+ // [0x00]
+ } else if (srcData.size == 1 && srcData[0].toInt() == 0) {
+ return srcData
+
+ // [0x01, 0x7f] - single byte, that byte is its own RLP encoding
+ } else if (srcData.size == 1 && srcData[0].toInt() and 0xFF < 0x80) {
+ return srcData
+
+ // [0x80, 0xb7], 0 - 55 bytes
+ } else if (srcData.size < SIZE_THRESHOLD) {
+ // length = 8X
+ val length = (OFFSET_SHORT_ITEM + srcData.size).toByte()
+ val data = Arrays.copyOf(srcData, srcData.size + 1)
+ System.arraycopy(data, 0, data, 1, srcData.size)
+ data[0] = length
+
+ return data
+ // [0xb8, 0xbf], 56+ bytes
+ } else {
+ // length of length = BX
+ // prefix = [BX, [length]]
+ var tmpLength = srcData.size
+ var lengthOfLength: Byte = 0
+ while (tmpLength != 0) {
+ ++lengthOfLength
+ tmpLength = tmpLength shr 8
+ }
+
+ // set length Of length at first byte
+ val data = ByteArray(1 + lengthOfLength.toInt() + srcData.size)
+ data[0] = (OFFSET_LONG_ITEM + lengthOfLength).toByte()
+
+ // copy length after first byte
+ tmpLength = srcData.size
+ for (i in lengthOfLength downTo 1) {
+ data[i] = (tmpLength and 0xFF).toByte()
+ tmpLength = tmpLength shr 8
+ }
+
+ // at last copy the number bytes after its length
+ System.arraycopy(srcData, 0, data, 1 + lengthOfLength, srcData.size)
+
+ return data
+ }
+ }
+
+ private fun encodeShort(singleShort: Short) =
+ if ((singleShort and 0xFF) == singleShort)
+ encodeByte(singleShort.toByte())
+ else {
+ byteArrayOf((OFFSET_SHORT_ITEM + 2).toByte(), (singleShort.toInt() shr 8 and 0xFF).toByte(), (singleShort.toInt() shr 0 and 0xFF).toByte())
+ }
+
+ fun encodeList(vararg elements: ByteArray): ByteArray {
+ var totalLength = 0
+ for (element1 in elements) {
+ totalLength += element1.size
+ }
+
+ val data: ByteArray
+ var copyPos: Int
+ if (totalLength < SIZE_THRESHOLD) {
+
+ data = ByteArray(1 + totalLength)
+ data[0] = (OFFSET_SHORT_LIST + totalLength).toByte()
+ copyPos = 1
+ } else {
+ var tmpLength = totalLength
+ var byteNum: Byte = 0
+ while (tmpLength != 0) {
+ ++byteNum
+ tmpLength = tmpLength shr 8
+ }
+ tmpLength = totalLength
+ val lenBytes = ByteArray(byteNum.toInt())
+ for (i in 0 until byteNum) {
+ lenBytes[byteNum.toInt() - 1 - i] = (tmpLength shr 8 * i and 0xFF).toByte()
+ }
+ data = ByteArray(1 + lenBytes.size + totalLength)
+ data[0] = (OFFSET_LONG_LIST + byteNum).toByte()
+ System.arraycopy(lenBytes, 0, data, 1, lenBytes.size)
+
+ copyPos = lenBytes.size + 1
+ }
+ for (element in elements) {
+ System.arraycopy(element, 0, data, copyPos, element.size)
+ copyPos += element.size
+ }
+ return data
+ }
+
+ fun decode(data: ByteArray, posParam: Int): DecodeResult {
+ var pos = posParam
+ val prefix = data[pos].toInt() and 0xFF
+
+ when {
+ prefix == OFFSET_SHORT_ITEM -> return DecodeResult(pos + 1, "")
+ prefix < OFFSET_SHORT_ITEM -> return DecodeResult(pos + 1, byteArrayOf(data[pos]))
+ prefix <= OFFSET_LONG_ITEM -> {
+ val len = prefix - OFFSET_SHORT_ITEM
+ return DecodeResult(pos + 1 + len, copyOfRange(data, pos + 1, pos + 1 + len))
+ }
+ prefix < OFFSET_SHORT_LIST -> {
+ val lenlen = prefix - OFFSET_LONG_ITEM
+ val lenbytes = copyOfRange(data, pos + 1, pos + 1 + lenlen).toInt()
+
+ return DecodeResult(pos + 1 + lenlen + lenbytes, copyOfRange(data, pos + 1 + lenlen, pos + 1 + lenlen
+ + lenbytes))
+ }
+ prefix <= OFFSET_LONG_LIST -> {
+ val len = prefix - OFFSET_SHORT_LIST
+ val prevPos = pos
+ pos++
+ return decodeList(data, pos, prevPos, len)
+ }
+ prefix <= 0xFF -> {
+ val lenlen = prefix - OFFSET_LONG_LIST
+ val lenlist = copyOfRange(data, pos + 1, pos + 1 + lenlen).toInt()
+ pos += lenlen + 1
+ return decodeList(data, pos, lenlist, lenlist)
+ }
+ else -> throw RuntimeException("Only byte values between 0x00 and 0xFF are supported, but got: $prefix")
+ }
+ }
+
+ fun decode2(msgData: ByteArray): RLPList {
+ val rlpList = RLPList()
+ fullTraverse(msgData, 0, 0, msgData.size, rlpList, Integer.MAX_VALUE)
+ return rlpList
+ }
+
+ private fun decodeList(data: ByteArray, posParam: Int, prevPosParam: Int, len: Int): DecodeResult {
+ var pos = posParam
+ var prevPos = prevPosParam
+
+ val slice = ArrayList()
+ var i = 0
+ while (i < len) {
+ val result = decode(data, pos)
+ slice.add(result.decoded)
+ prevPos = result.pos
+ i += prevPos - pos
+ pos = prevPos
+ }
+ return DecodeResult(pos, slice.toTypedArray())
+ }
+
+ private fun encodeLength(length: Int, offset: Int) = when {
+ length < SIZE_THRESHOLD -> {
+ val firstByte = (length + offset).toByte()
+ byteArrayOf(firstByte)
+ }
+ length < MAX_ITEM_LENGTH -> {
+ val binaryLength: ByteArray
+ if (length > 0xFF)
+ binaryLength = length.toBytesNoLeadZeroes()
+ else
+ binaryLength = byteArrayOf(length.toByte())
+ val firstByte = (binaryLength.size + offset + SIZE_THRESHOLD - 1).toByte()
+ concatenate(byteArrayOf(firstByte), binaryLength)
+ }
+ else -> throw RuntimeException("Input too long")
+ }
+
+ private fun toBytes(input: Any?): ByteArray = when (input) {
+ is ByteArray -> input
+ is String -> input.toByteArray()
+ is Long -> if (input == 0) byteArrayOf() else asUnsignedByteArray(BigInteger.valueOf(input))
+ is Int -> if (input == 0) byteArrayOf() else asUnsignedByteArray(BigInteger.valueOf(input.toLong()))
+ is BigInteger -> if (input == BigInteger.ZERO) byteArrayOf() else asUnsignedByteArray(input)
+ is Value -> toBytes(input.asObj())
+ else -> throw RuntimeException("Unsupported type: Only accepting String, Integer and BigInteger for now")
+ }
+
+ fun fullTraverse(msgData: ByteArray?, level: Int, startPos: Int,
+ endPos: Int, rlpList: RLPList, depth: Int) {
+ if (level > MAX_DEPTH) {
+ throw RuntimeException(String.format("Error: Traversing over max RLP depth (%s)", MAX_DEPTH))
+ }
+
+ try {
+ if (msgData == null || msgData.isEmpty())
+ return
+ var pos = startPos
+
+ while (pos < endPos) {
+
+ // It's a list with a payload more than 55 bytes
+ // data[0] - 0xF7 = how many next bytes allocated
+ // for the length of the list
+ if (msgData[pos].toInt() and 0xFF > OFFSET_LONG_LIST) {
+
+ val lengthOfLength = ((msgData[pos].toInt() and 0xFF) - OFFSET_LONG_LIST).toByte()
+ val length = calcLength(lengthOfLength.toInt(), msgData, pos)
+
+ if (length < SIZE_THRESHOLD) {
+ throw RuntimeException("Short list has been encoded as long list")
+ }
+
+ val rlpData = ByteArray(lengthOfLength.toInt() + length + 1)
+ System.arraycopy(msgData, pos, rlpData, 0, lengthOfLength.toInt()
+ + length + 1)
+
+ if (level + 1 < depth) {
+ val newLevelList = RLPList()
+ newLevelList.rlpData = rlpData
+
+ fullTraverse(msgData, level + 1, pos + lengthOfLength.toInt() + 1,
+ pos + lengthOfLength.toInt() + length + 1, newLevelList, depth)
+ rlpList.add(newLevelList)
+ } else {
+ rlpList.add(RLPItem(rlpData))
+ }
+
+ pos += lengthOfLength.toInt() + length + 1
+ continue
+ }
+ // It's a list with a payload less than 55 bytes
+ if (msgData[pos].toInt() and 0xFF in OFFSET_SHORT_LIST..OFFSET_LONG_LIST) {
+
+ val length = ((msgData[pos].toInt() and 0xFF) - OFFSET_SHORT_LIST).toByte()
+
+ val rlpData = ByteArray(length + 1)
+ System.arraycopy(msgData, pos, rlpData, 0, length + 1)
+
+ if (level + 1 < depth) {
+ val newLevelList = RLPList()
+ newLevelList.rlpData = rlpData
+
+ if (length > 0)
+ fullTraverse(msgData, level + 1, pos + 1, pos + length.toInt() + 1, newLevelList, depth)
+ rlpList.add(newLevelList)
+ } else {
+ rlpList.add(RLPItem(rlpData))
+ }
+
+ pos += 1 + length
+ continue
+ }
+ // It's an item with a payload more than 55 bytes
+ // data[0] - 0xB7 = how much next bytes allocated for
+ // the length of the string
+ if (msgData[pos].toInt() and 0xFF in (OFFSET_LONG_ITEM + 1)..(OFFSET_SHORT_LIST - 1)) {
+
+ val lengthOfLength = (msgData[pos].toInt() and 0xFF) - OFFSET_LONG_ITEM
+ val length = calcLength(lengthOfLength, msgData, pos)
+ if (length < SIZE_THRESHOLD) {
+ throw RuntimeException("Short item has been encoded as long item")
+ }
+
+ // now we can parse an item for data[1]..data[length]
+ val item = ByteArray(length)
+ System.arraycopy(msgData, pos + lengthOfLength.toInt() + 1, item,
+ 0, length)
+
+ val rlpItem = RLPItem(item)
+ rlpList.add(rlpItem)
+ pos += lengthOfLength + length + 1
+
+ continue
+ }
+ // It's an item less than 55 bytes long,
+ // data[0] - 0x80 == length of the item
+ if (msgData[pos].toInt() and 0xFF in (OFFSET_SHORT_ITEM + 1)..OFFSET_LONG_ITEM) {
+
+ val length = (msgData[pos].toInt() and 0xFF) - OFFSET_SHORT_ITEM
+
+ val item = ByteArray(length)
+ System.arraycopy(msgData, pos + 1, item, 0, length.toInt())
+
+ if (length == 1 && item[0].toInt() and 0xFF < OFFSET_SHORT_ITEM) {
+ throw RuntimeException("Single byte has been encoded as byte string")
+ }
+
+ val rlpItem = RLPItem(item)
+ rlpList.add(rlpItem)
+ pos += 1 + length
+
+ continue
+ }
+ // null item
+ if ((msgData[pos].toInt() and 0xFF) == OFFSET_SHORT_ITEM) {
+ val item = byteArrayOf()
+ val rlpItem = RLPItem(item)
+ rlpList.add(rlpItem)
+ pos += 1
+ continue
+ }
+ // single byte item
+ if (msgData[pos].toInt() and 0xFF < OFFSET_SHORT_ITEM) {
+
+ val item = byteArrayOf((msgData[pos].toInt() and 0xFF).toByte())
+
+ val rlpItem = RLPItem(item)
+ rlpList.add(rlpItem)
+ pos += 1
+ }
+ }
+ } catch (e: Exception) {
+ throw RuntimeException("RLP wrong encoding (" + Hex.toHexString(msgData, startPos, endPos - startPos) + ")", e)
+ } catch (e: OutOfMemoryError) {
+ throw RuntimeException("Invalid RLP (excessive mem allocation while parsing) (" + Hex.toHexString(msgData, startPos, endPos - startPos) + ")", e)
+ }
+
+ }
+
+ private fun calcLength(lengthOfLength: Int, msgData: ByteArray, pos: Int): Int {
+ var pow = (lengthOfLength - 1).toByte()
+ var length = 0
+ for (i in 1..lengthOfLength) {
+
+ val bt = msgData[pos + i].toInt() and 0xFF
+ val shift = 8 * pow
+
+ // no leading zeros are acceptable
+ if (bt == 0 && length == 0) {
+ throw RuntimeException("RLP length contains leading zeros")
+ }
+
+ // return MAX_VALUE if index of highest bit is more than 31
+ if (32 - Integer.numberOfLeadingZeros(bt) + shift > 31) {
+ return Integer.MAX_VALUE
+ }
+
+ length += bt shl shift
+ pow--
+ }
+
+ return length
+ }
+
+ fun decode2OneItem(msgData: ByteArray, startPos: Int): RLPElement {
+ val rlpList = RLPList()
+ fullTraverse(msgData, 0, startPos, startPos + 1, rlpList, Integer.MAX_VALUE)
+ return rlpList[0]
+ }
+
+ fun rlpDecodeInt(elem: RLPElement): Int {
+ val b = elem.rlpData
+ return b.toInt()
+ }
+
+ fun decodeLong(data: ByteArray, index: Int): Long {
+ var value: Long = 0
+ when {
+ data[index].toInt() == 0x00 -> throw RuntimeException("not a number")
+ data[index].toInt() and 0xFF < OFFSET_SHORT_ITEM -> return data[index].toLong()
+ data[index].toInt() and 0xFF <= OFFSET_SHORT_ITEM + java.lang.Long.BYTES -> {
+
+ val length = ((data[index].toInt() and 0xFF) - OFFSET_SHORT_ITEM)
+ var pow = (length - 1).toByte()
+ for (i in 1..length) {
+ // << (8 * pow) == bit shift to 0 (*1), 8 (*256) , 16 (*65..)..
+ value += (data[index + i].toInt() and 0xFF).toLong() shl 8 * pow
+ pow--
+ }
+ }
+ else -> // If there are more than 8 bytes, it is not going
+ // to decode properly into a long.
+ throw RuntimeException("wrong decode attempt")
+ }
+ return value
+ }
+
+ fun getNextElementIndex(payload: ByteArray, pos: Int): Int {
+ if (pos >= payload.size)
+ return -1
+
+ // [0xf8, 0xff]
+ if (payload[pos].toInt() and 0xFF > OFFSET_LONG_LIST) {
+ val lengthOfLength = ((payload[pos].toInt() and 0xFF) - OFFSET_LONG_LIST).toByte()
+ val length = calcLength(lengthOfLength.toInt(), payload, pos)
+ return pos + lengthOfLength.toInt() + length + 1
+ }
+ // [0xc0, 0xf7]
+ if (payload[pos].toInt() and 0xFF in OFFSET_SHORT_LIST..OFFSET_LONG_LIST) {
+
+ val length = ((payload[pos].toInt() and 0xFF) - OFFSET_SHORT_LIST).toByte()
+ return pos + 1 + length.toInt()
+ }
+ // [0xb8, 0xbf]
+ if (payload[pos].toInt() and 0xFF in (OFFSET_LONG_ITEM + 1)..(OFFSET_SHORT_LIST - 1)) {
+
+ val lengthOfLength = ((payload[pos].toInt() and 0xFF) - OFFSET_LONG_ITEM).toByte()
+ val length = calcLength(lengthOfLength.toInt(), payload, pos)
+ return pos + lengthOfLength.toInt() + length + 1
+ }
+ // [0x81, 0xb7]
+ if (payload[pos].toInt() and 0xFF in (OFFSET_SHORT_ITEM + 1)..OFFSET_LONG_ITEM) {
+
+ val length = ((payload[pos].toInt() and 0xFF) - OFFSET_SHORT_ITEM).toByte()
+ return pos + 1 + length.toInt()
+ }
+ // []0x80]
+ if (payload[pos].toInt() and 0xFF == OFFSET_SHORT_ITEM) {
+ return pos + 1
+ }
+ // [0x00, 0x7f]
+ return if (payload[pos].toInt() and 0xFF < OFFSET_SHORT_ITEM) {
+ pos + 1
+ } else -1
+ }
+}
\ No newline at end of file
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/rlp/Value.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/rlp/Value.kt
new file mode 100644
index 00000000..26be3adc
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/light/rlp/Value.kt
@@ -0,0 +1,57 @@
+package io.horizontalsystems.ethereumkit.light.rlp
+
+import java.util.*
+
+class Value(obj: Any?) {
+
+ private var value: Any? = null
+ private var rlp: ByteArray? = null
+ private var sha3: ByteArray? = null
+
+ private var decoded = false
+
+
+ init {
+ this.decoded = true
+ if (obj is Value) {
+ this.value = obj.asObj()
+ } else {
+ this.value = obj
+ }
+ }
+
+ constructor() : this(null)
+
+ fun init(rlp: ByteArray) {
+ this.rlp = rlp
+ }
+
+ fun withHash(hash: ByteArray): Value {
+ sha3 = hash
+ return this
+ }
+
+ fun asObj(): Any? {
+ decode()
+ return value
+ }
+
+ fun asList(): List {
+ decode()
+ val valueArray = value as Array
+ return Arrays.asList(*valueArray)
+ }
+
+ fun decode() {
+ if (!this.decoded) {
+ this.value = rlp?.let { RLP.decode(it, 0).decoded }
+ this.decoded = true
+ }
+ }
+
+ fun isList(): Boolean {
+ decode()
+ return value?.let { it.javaClass.isArray && !it.javaClass.componentType.isPrimitive }
+ ?: false
+ }
+}
\ No newline at end of file
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/Balance.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/Balance.kt
deleted file mode 100644
index f82effff..00000000
--- a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/Balance.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-package io.horizontalsystems.ethereumkit.models
-
-import io.realm.RealmObject
-import io.realm.annotations.PrimaryKey
-
-open class Balance : RealmObject {
-
- @PrimaryKey
- var address = ""
-
- var balance: Double = 0.0
-
- constructor()
-
- constructor(address: String, balance: Double) {
- this.address = address
- this.balance = balance
- }
-
-}
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/EthereumBalance.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/EthereumBalance.kt
new file mode 100644
index 00000000..b493db86
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/EthereumBalance.kt
@@ -0,0 +1,11 @@
+package io.horizontalsystems.ethereumkit.models
+
+import android.arch.persistence.room.Entity
+import android.arch.persistence.room.PrimaryKey
+
+@Entity
+data class EthereumBalance(
+ @PrimaryKey
+ val address: String,
+ val balance: String
+)
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/EthereumTransaction.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/EthereumTransaction.kt
new file mode 100644
index 00000000..2ddb9b43
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/EthereumTransaction.kt
@@ -0,0 +1,53 @@
+package io.horizontalsystems.ethereumkit.models
+
+import android.arch.persistence.room.Entity
+import io.horizontalsystems.ethereumkit.models.etherscan.EtherscanTransaction
+import org.web3j.crypto.Keys
+
+@Entity(primaryKeys = ["hash","contractAddress"])
+class EthereumTransaction() {
+
+ constructor(etherscanTx: EtherscanTransaction) : this() {
+ hash = etherscanTx.hash
+ nonce = etherscanTx.nonce.toIntOrNull() ?: 0
+ input = etherscanTx.input
+ from = formatInEip55(etherscanTx.from)
+ to = formatInEip55(etherscanTx.to)
+ contractAddress = formatInEip55(etherscanTx.contractAddress)
+ blockNumber = etherscanTx.blockNumber.toLongOrNull()
+ blockHash = etherscanTx.blockHash
+ value = etherscanTx.value
+ gasLimit = etherscanTx.gas.toIntOrNull() ?: 0
+ gasPriceInWei = etherscanTx.gasPrice.toLongOrNull() ?: 0L
+ timeStamp = etherscanTx.timeStamp.toLongOrNull() ?: 0
+ transactionIndex = etherscanTx.transactionIndex
+ iserror = etherscanTx.isError ?: ""
+ txReceiptStatus = etherscanTx.txreceipt_status ?: ""
+ cumulativeGasUsed = etherscanTx.cumulativeGasUsed
+ gasUsed = etherscanTx.gasUsed
+ confirmations = etherscanTx.confirmations.toLongOrNull() ?: 0
+ }
+
+ var hash: String = ""
+ var nonce: Int = 0
+ var input: String = ""
+ var from: String = ""
+ var to: String = ""
+ var value: String = ""
+ var gasLimit: Int = 0
+ var gasPriceInWei: Long = 0
+ var timeStamp: Long = 0
+ var contractAddress: String = ""
+ var blockHash: String = ""
+ var blockNumber: Long? = null
+ var confirmations: Long = 0
+ var gasUsed: String = ""
+ var cumulativeGasUsed: String = ""
+ var iserror: String = ""
+ var transactionIndex: String = ""
+ var txReceiptStatus: String = ""
+
+ private fun formatInEip55(textString: String) =
+ if (textString.isEmpty()) "" else Keys.toChecksumAddress(textString)
+
+}
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/GasPrice.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/GasPrice.kt
index d804ed68..a06e967d 100644
--- a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/GasPrice.kt
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/GasPrice.kt
@@ -1,19 +1,7 @@
package io.horizontalsystems.ethereumkit.models
-import io.realm.RealmObject
-import io.realm.annotations.PrimaryKey
+import android.arch.persistence.room.Entity
+import android.arch.persistence.room.PrimaryKey
-open class GasPrice : RealmObject {
-
- @PrimaryKey
- var id = ""
-
- var gasPriceInGwei: Double = 0.0
-
- constructor()
-
- constructor(gasPriceInGwei: Double) {
- this.gasPriceInGwei = gasPriceInGwei
- }
-
-}
+@Entity
+data class GasPrice(val gasPriceInWei: Long, @PrimaryKey val id: String = "")
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/LastBlockHeight.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/LastBlockHeight.kt
index b94258ed..6f5de670 100644
--- a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/LastBlockHeight.kt
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/LastBlockHeight.kt
@@ -1,19 +1,7 @@
package io.horizontalsystems.ethereumkit.models
-import io.realm.RealmObject
-import io.realm.annotations.PrimaryKey
+import android.arch.persistence.room.Entity
+import android.arch.persistence.room.PrimaryKey
-open class LastBlockHeight : RealmObject {
-
- @PrimaryKey
- var id = ""
-
- var height: Int = 0
-
- constructor()
-
- constructor(height: Int) {
- this.height = height
- }
-
-}
+@Entity
+class LastBlockHeight(val height: Int, @PrimaryKey val id: String = "")
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/Network.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/Network.kt
new file mode 100644
index 00000000..e7dac36b
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/Network.kt
@@ -0,0 +1,3 @@
+package io.horizontalsystems.ethereumkit.models
+
+enum class NetworkType { MainNet, Ropsten, Kovan, Rinkeby }
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/State.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/State.kt
new file mode 100644
index 00000000..f8b35422
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/State.kt
@@ -0,0 +1,52 @@
+package io.horizontalsystems.ethereumkit.models
+
+import io.horizontalsystems.ethereumkit.EthereumKit
+import io.horizontalsystems.ethereumkit.core.ERC20
+import java.util.concurrent.ConcurrentHashMap
+
+class State {
+ var balance: String? = null
+ var lastBlockHeight: Int? = null
+
+ val erc20List = ConcurrentHashMap()
+
+ val erc20Listeners: List
+ get() {
+ val listeners = mutableListOf()
+ erc20List.values.forEach {
+ listeners.add(it.listener)
+ }
+ return listeners
+ }
+
+ fun clear() {
+ balance = null
+ lastBlockHeight = null
+ erc20List.clear()
+ }
+
+ fun add(contractAddress: String, listener: EthereumKit.Listener) {
+ erc20List[contractAddress] = ERC20(contractAddress, listener)
+ }
+
+ fun hasContract(contractAddress: String): Boolean {
+ return erc20List.containsKey(contractAddress)
+ }
+
+ fun remove(contractAddress: String) {
+ erc20List.remove(contractAddress)
+ }
+
+ fun balance(contractAddress: String): String? {
+ return erc20List[contractAddress]?.balance
+ }
+
+ fun listener(contractAddress: String): EthereumKit.Listener? {
+ return erc20List[contractAddress]?.listener
+ }
+
+ fun setBalance(balance: String?, contractAddress: String) {
+ erc20List[contractAddress]?.balance = balance
+ }
+
+}
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/Transaction.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/Transaction.kt
deleted file mode 100644
index fae179e7..00000000
--- a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/Transaction.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-package io.horizontalsystems.ethereumkit.models
-
-import io.horizontalsystems.ethereumkit.models.etherscan.EtherscanTransaction
-import io.realm.RealmObject
-import io.realm.annotations.PrimaryKey
-
-open class Transaction : RealmObject {
-
- @PrimaryKey
- var hash: String = ""
- var timeStamp: Long = 0
-
- var from: String = ""
- var to: String = ""
-
- var value: String = ""
- var gas: Int = 0
- var gasPrice: String = ""
-
- var blockNumber: Long = 0
- var blockHash: String = ""
-
- var nonce: Int = 0
- var transactionIndex: String = ""
- var isError: String = ""
- var txReceiptStatus: String = ""
- var input: String = ""
- var contractAddress: String = ""
- var cumulativeGasUsed: String = ""
- var gasUsed: String = ""
- var confirmations: Int = 0
-
- constructor()
-
- constructor(etherscanTx: EtherscanTransaction) {
- this.hash = etherscanTx.hash
- this.timeStamp = etherscanTx.timeStamp.toLongOrNull() ?: 0
-
- this.from = etherscanTx.from
- this.to = etherscanTx.to
-
- this.value = etherscanTx.value
- this.gas = etherscanTx.gas.toIntOrNull() ?: 0
- this.gasPrice = etherscanTx.gasPrice
-
- this.blockNumber = etherscanTx.blockNumber.toLongOrNull() ?: 0
- this.blockHash = etherscanTx.blockHash
-
- this.nonce = etherscanTx.nonce.toIntOrNull() ?: 0
- this.transactionIndex = etherscanTx.transactionIndex
- this.isError = etherscanTx.isError
- this.txReceiptStatus = etherscanTx.txreceipt_status
- this.input = etherscanTx.input
- this.contractAddress = etherscanTx.contractAddress
- this.cumulativeGasUsed = etherscanTx.cumulativeGasUsed
- this.gasUsed = etherscanTx.gasUsed
- this.confirmations = etherscanTx.confirmations.toIntOrNull() ?: 0
- }
-
-}
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/etherscan/EtherscanTransaction.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/etherscan/EtherscanTransaction.kt
index 67e3413a..6d7aefce 100644
--- a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/etherscan/EtherscanTransaction.kt
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/etherscan/EtherscanTransaction.kt
@@ -12,8 +12,8 @@ data class EtherscanTransaction(
val value: String,
val gas: String,
val gasPrice: String,
- val isError: String,
- val txreceipt_status: String,
+ val isError: String?,
+ val txreceipt_status: String?,
val input: String,
val contractAddress: String,
val cumulativeGasUsed: String,
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/network/Configuration.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/network/Configuration.kt
new file mode 100644
index 00000000..2a90f0eb
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/network/Configuration.kt
@@ -0,0 +1,24 @@
+package io.horizontalsystems.ethereumkit.network
+
+import io.horizontalsystems.ethereumkit.models.NetworkType
+
+class Configuration(val networkType: NetworkType, val infuraKey: String, val etherscanAPIKey: String, val debugPrints: Boolean) {
+ val etherScanUrl: String
+ get() {
+ return when (networkType) {
+ NetworkType.MainNet -> "https://api.etherscan.io"
+ NetworkType.Ropsten -> "https://api-ropsten.etherscan.io"
+ NetworkType.Kovan -> "https://api-kovan.etherscan.io"
+ NetworkType.Rinkeby -> "https://api-rinkeby.etherscan.io"
+ }
+ }
+
+ private val subDomain = when (networkType) {
+ NetworkType.MainNet -> "mainnet"
+ NetworkType.Kovan -> "kovan"
+ NetworkType.Rinkeby -> "rinkeby"
+ NetworkType.Ropsten -> "ropsten"
+ }
+
+ val infuraUrl: String = "https://$subDomain.infura.io/$infuraKey"
+}
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/network/EtherscanService.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/network/EtherscanService.kt
index 0d9db8f0..844160b0 100644
--- a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/network/EtherscanService.kt
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/network/EtherscanService.kt
@@ -1,28 +1,21 @@
package io.horizontalsystems.ethereumkit.network
import com.google.gson.GsonBuilder
-import io.horizontalsystems.ethereumkit.EthereumKit.NetworkType
import io.horizontalsystems.ethereumkit.models.etherscan.EtherscanResponse
+import io.reactivex.Flowable
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
-import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
+import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query
-import rx.Observable
-class EtherscanService(networkType: NetworkType, private val apiKey: String) {
+class EtherscanService(baseUrl: String, private val apiKey: String) {
private val service: EtherscanServiceAPI
init {
- val baseUrl = when (networkType) {
- NetworkType.MainNet -> "https://api.etherscan.io"
- NetworkType.Ropsten -> "https://api-ropsten.etherscan.io"
- NetworkType.Kovan -> "https://api-kovan.etherscan.io"
- NetworkType.Rinkeby -> "https://api-rinkeby.etherscan.io"
- }
val logger = HttpLoggingInterceptor()
.setLevel(HttpLoggingInterceptor.Level.BASIC)
@@ -36,7 +29,7 @@ class EtherscanService(networkType: NetworkType, private val apiKey: String) {
val retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
- .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
+ .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.client(httpClient.build())
.build()
@@ -44,9 +37,13 @@ class EtherscanService(networkType: NetworkType, private val apiKey: String) {
service = retrofit.create(EtherscanServiceAPI::class.java)
}
- fun getTransactionList(address: String, startBlock: Int): Observable =
- service.getTransactionList("account", "txList", address, startBlock, 99_999_999, "desc", apiKey)
+ fun getTransactionList(address: String, startBlock: Int): Flowable {
+ return service.getTransactionList("account", "txList", address, startBlock, 99_999_999, "desc", apiKey)
+ }
+ fun getTokenTransactions(address: String, startBlock: Int): Flowable {
+ return service.getTokenTransactions("account", "tokentx", address, startBlock, 99_999_999, "desc", apiKey)
+ }
interface EtherscanServiceAPI {
@@ -58,7 +55,17 @@ class EtherscanService(networkType: NetworkType, private val apiKey: String) {
@Query("startblock") startblock: Int,
@Query("endblock") endblock: Int,
@Query("sort") sort: String,
- @Query("apiKey") apiKey: String): Observable
+ @Query("apiKey") apiKey: String): Flowable
+
+ @GET("/api")
+ fun getTokenTransactions(
+ @Query("module") module: String,
+ @Query("action") action: String,
+ @Query("address") address: String,
+ @Query("startblock") startblock: Int,
+ @Query("endblock") endblock: Int,
+ @Query("sort") sort: String,
+ @Query("apiKey") apiKey: String): Flowable
}
}
diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/utils/MainThreadExecutor.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/utils/MainThreadExecutor.kt
new file mode 100644
index 00000000..38154523
--- /dev/null
+++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/utils/MainThreadExecutor.kt
@@ -0,0 +1,14 @@
+package io.horizontalsystems.ethereumkit.utils
+
+import android.os.Handler
+import android.os.Looper
+import java.util.concurrent.Executor
+
+class MainThreadExecutor : Executor {
+
+ private val uiHandler = Handler(Looper.getMainLooper())
+
+ override fun execute(command: Runnable) {
+ uiHandler.post(command)
+ }
+}
diff --git a/ethereumkit/src/test/java/io/horizontalsystems/ethereumkit/EthereumKitTest.kt b/ethereumkit/src/test/java/io/horizontalsystems/ethereumkit/EthereumKitTest.kt
new file mode 100644
index 00000000..ceefd6c7
--- /dev/null
+++ b/ethereumkit/src/test/java/io/horizontalsystems/ethereumkit/EthereumKitTest.kt
@@ -0,0 +1,417 @@
+package io.horizontalsystems.ethereumkit
+
+import com.nhaarman.mockito_kotlin.any
+import com.nhaarman.mockito_kotlin.never
+import com.nhaarman.mockito_kotlin.verify
+import com.nhaarman.mockito_kotlin.whenever
+import io.horizontalsystems.ethereumkit.core.AddressValidator
+import io.horizontalsystems.ethereumkit.core.IBlockchain
+import io.horizontalsystems.ethereumkit.core.IStorage
+import io.horizontalsystems.ethereumkit.models.EthereumTransaction
+import io.horizontalsystems.ethereumkit.models.State
+import io.reactivex.Single
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.mock
+import org.web3j.utils.Convert
+import java.math.BigDecimal
+import java.util.concurrent.Executor
+
+class EthereumKitTest {
+
+ private val blockchain = mock(IBlockchain::class.java)
+ private val storage = mock(IStorage::class.java)
+ private val addressValidator = mock(AddressValidator::class.java)
+ private val state = mock(State::class.java)
+ private val listener = mock(EthereumKit.Listener::class.java)
+ private lateinit var kit: EthereumKit
+
+ private val ethereumAddress = "ether"
+
+ private val transaction = EthereumTransaction().apply {
+ hash = "hash"
+ nonce = 123
+ input = "input"
+ from = "from"
+ to = "to"
+ value = "3.0"
+ }
+
+
+ @Before
+ fun setUp() {
+ RxBaseTest.setup()
+
+ whenever(storage.getBalance(any())).thenReturn(null)
+ whenever(blockchain.ethereumAddress).thenReturn(ethereumAddress)
+ kit = EthereumKit(blockchain, storage, addressValidator, state)
+ kit.listener = listener
+ kit.listenerExecutor = Executor {
+ it.run()
+ }
+ }
+
+ @Test
+ fun testInit_balance() {
+ val balance = "123.45"
+ val lastBlockHeight = 123
+
+ whenever(storage.getBalance(ethereumAddress)).thenReturn(balance)
+ whenever(storage.getLastBlockHeight()).thenReturn(lastBlockHeight)
+
+ kit = EthereumKit(blockchain, storage, addressValidator, state)
+
+ verify(state).balance = balance
+ verify(state).lastBlockHeight = lastBlockHeight
+ }
+
+ @Test
+ fun testStart() {
+ kit.start()
+ verify(blockchain).start()
+ }
+
+ @Test
+ fun testStop() {
+ kit.stop()
+ verify(blockchain).stop()
+ }
+
+ @Test
+ fun testClear() {
+ kit.clear()
+ verify(blockchain).clear()
+ verify(state).clear()
+ verify(storage).clear()
+ }
+
+ @Test
+ fun testReceiveAddress() {
+ val ethereumAddress = "eth_address"
+ whenever(blockchain.ethereumAddress).thenReturn(ethereumAddress)
+ Assert.assertEquals(ethereumAddress, kit.receiveAddress)
+ }
+
+ @Test
+ fun testRegister() {
+ val address = "address"
+ val balance = "123"
+ val listenerMock = mock(EthereumKit.Listener::class.java)
+ whenever(storage.getBalance(address)).thenReturn(balance)
+ whenever(state.hasContract(address)).thenReturn(false)
+ kit.register(address, listenerMock)
+
+ verify(state).add(contractAddress = address, listener = listenerMock)
+ verify(state).setBalance(balance, address)
+ verify(blockchain).register(contractAddress = address)
+ }
+
+ @Test
+ fun testRegister_alreadyExists() {
+ val address = "address"
+ val listenerMock = mock(EthereumKit.Listener::class.java)
+
+ whenever(state.hasContract(address)).thenReturn(true)
+ kit.register(address, listenerMock)
+
+ verify(state, never()).add(contractAddress = address, listener = listenerMock)
+ verify(blockchain, never()).register(contractAddress = address)
+ }
+
+ @Test
+ fun testUnregister() {
+ val address = "address"
+ kit.unregister(address)
+
+ verify(state).remove(address)
+ verify(blockchain).unregister(address)
+ }
+
+ @Test
+ fun testValidateAddress() {
+ val address = "address"
+
+ kit.validateAddress(address)
+
+ verify(addressValidator).validate(address)
+ }
+
+ @Test(expected = AddressValidator.AddressValidationException::class)
+ fun testValidateAddress_failed() {
+ val address = "address"
+
+ whenever(addressValidator.validate(address)).thenThrow(AddressValidator.AddressValidationException(""))
+
+ kit.validateAddress(address)
+ }
+
+ @Test
+ fun testFee() {
+ val gasLimit = 21_000
+ val gasPrice = 123L
+
+ whenever(blockchain.gasPriceInWei).thenReturn(gasPrice)
+ whenever(blockchain.gasLimitEthereum).thenReturn(gasLimit)
+
+ val gas = BigDecimal.valueOf(gasPrice)
+ val expectedFee = Convert.fromWei(gas.multiply(blockchain.gasLimitEthereum.toBigDecimal()), Convert.Unit.ETHER)
+
+ val fee = kit.fee()
+
+ Assert.assertEquals(expectedFee, fee)
+ }
+
+ @Test
+ fun testFee_customGasPrice() {
+ val gasLimit = 21_000
+ val customGasPrice = 23L
+
+ whenever(blockchain.gasLimitEthereum).thenReturn(gasLimit)
+
+ val gas = BigDecimal.valueOf(customGasPrice)
+ val expectedFee = Convert.fromWei(gas.multiply(blockchain.gasLimitEthereum.toBigDecimal()), Convert.Unit.ETHER)
+
+ val fee = kit.fee(customGasPrice)
+
+ Assert.assertEquals(expectedFee, fee)
+ }
+
+ @Test
+ fun testTransactions() {
+ val fromHash = "hash"
+ val limit = 5
+ val expectedResult = Single.just(listOf())
+
+ whenever(storage.getTransactions(fromHash, limit, null)).thenReturn(expectedResult)
+
+ val result = kit.transactions(fromHash, limit)
+
+ Assert.assertEquals(expectedResult, result)
+ }
+
+ @Test
+ fun testSend_gasPriceNull() {
+ val amount = "23.4"
+ val gasPrice = null
+ val toAddress = "address"
+
+ val expectedResult = Single.just(transaction)
+
+ whenever(blockchain.send(toAddress, amount, gasPrice)).thenReturn(expectedResult)
+
+ val result = kit.send(toAddress, amount, gasPrice)
+
+ Assert.assertEquals(expectedResult, result)
+ }
+
+ @Test
+ fun testSend_withCustomGasPrice() {
+ val amount = "23.4"
+ val gasPrice = 34L
+ val toAddress = "address"
+
+ val expectedResult = Single.just(transaction)
+
+ whenever(blockchain.send(toAddress, amount, gasPrice)).thenReturn(expectedResult)
+
+ val result = kit.send(toAddress, amount, gasPrice)
+
+ Assert.assertEquals(expectedResult, result)
+ }
+
+ @Test
+ fun testBalance() {
+ val balance = "32.3"
+ whenever(state.balance).thenReturn(balance)
+ val result = kit.balance
+
+ Assert.assertEquals(balance, result)
+ }
+
+ @Test
+ fun testState_syncing() {
+ whenever(blockchain.blockchainSyncState).thenReturn(EthereumKit.SyncState.Syncing)
+ val result = kit.syncState
+
+ Assert.assertEquals(EthereumKit.SyncState.Syncing, result)
+ }
+
+ //
+ //Erc20
+ //
+
+
+ @Test
+ fun testErc20Fee() {
+ val erc20GasLimit = 100_000
+ val gasPrice = 1230000000L
+
+ whenever(blockchain.gasPriceInWei).thenReturn(gasPrice)
+ whenever(blockchain.gasLimitErc20).thenReturn(erc20GasLimit)
+
+ val gas = BigDecimal.valueOf(gasPrice)
+ val expectedFee = Convert.fromWei(gas.multiply(blockchain.gasLimitErc20.toBigDecimal()), Convert.Unit.ETHER)
+
+ val fee = kit.feeERC20()
+
+ Assert.assertEquals(expectedFee, fee)
+ }
+
+ @Test
+ fun testErc20Fee_customGasPrice() {
+ val erc20GasLimit = 100_000
+ val customGasPrice = 23L
+
+ whenever(blockchain.gasLimitErc20).thenReturn(erc20GasLimit)
+
+ val gas = BigDecimal.valueOf(customGasPrice)
+ val expectedFee = Convert.fromWei(gas.multiply(blockchain.gasLimitErc20.toBigDecimal()), Convert.Unit.ETHER)
+
+ val fee = kit.feeERC20(customGasPrice)
+
+ Assert.assertEquals(expectedFee, fee)
+ }
+
+ @Test
+ fun testErc20Balance() {
+ val balance = "23.03"
+ val address = "address"
+ whenever(state.balance(address)).thenReturn(balance)
+
+ val result = kit.balanceERC20(address)
+ Assert.assertEquals(balance, result)
+ }
+
+ @Test
+ fun testState_null() {
+ val address = "address"
+
+ whenever(blockchain.syncState(address)).thenReturn(EthereumKit.SyncState.NotSynced)
+
+ val result = kit.syncStateErc20(address)
+ Assert.assertEquals(EthereumKit.SyncState.NotSynced, result)
+ }
+
+ @Test
+ fun testErc20Transaction() {
+ val address = "address"
+ val fromHash = "hash"
+ val limit = 5
+ val expectedResult = Single.just(listOf())
+
+ whenever(storage.getTransactions(fromHash, limit, address)).thenReturn(expectedResult)
+
+ val result = kit.transactionsERC20(address, fromHash, limit)
+
+ Assert.assertEquals(expectedResult, result)
+ }
+
+ @Test
+ fun testErc20Send_gasPriceNull() {
+ val amount = "23.4"
+ val gasPrice = null
+ val toAddress = "address"
+ val contractAddress = "contAddress"
+
+ val expectedResult = Single.just(transaction)
+
+ whenever(blockchain.sendErc20(toAddress, contractAddress, amount, gasPrice)).thenReturn(expectedResult)
+
+ val result = kit.sendERC20(toAddress, contractAddress, amount, gasPrice)
+
+ Assert.assertEquals(expectedResult, result)
+ }
+
+ @Test
+ fun testErc20Send_withCustomGasPrice() {
+ val amount = "23.4"
+ val gasPrice = 234L
+ val toAddress = "address"
+ val contractAddress = "contAddress"
+
+ val expectedResult = Single.just(transaction)
+
+ whenever(blockchain.sendErc20(toAddress, contractAddress, amount, gasPrice)).thenReturn(expectedResult)
+
+ val result = kit.sendERC20(toAddress, contractAddress, amount, gasPrice)
+
+ Assert.assertEquals(expectedResult, result)
+ }
+
+ @Test
+ fun testOnUpdateLastBlockHeight() {
+ val height = 34
+ val erc20Listener = mock(EthereumKit.Listener::class.java)
+
+ whenever(state.erc20Listeners).thenReturn(listOf(erc20Listener))
+ kit.onUpdateLastBlockHeight(height)
+
+ verify(state).lastBlockHeight = height
+ verify(listener).onLastBlockHeightUpdate()
+ verify(erc20Listener).onLastBlockHeightUpdate()
+ }
+
+ @Test
+ fun testOnUpdateState() {
+ val syncState = EthereumKit.SyncState.Syncing
+ kit.onUpdateState(syncState)
+
+ verify(listener).onSyncStateUpdate()
+ }
+
+ @Test
+ fun testOnUpdateErc20State() {
+ val contractAddress = "address"
+ val syncState = EthereumKit.SyncState.Syncing
+ kit.onUpdateErc20State(syncState, contractAddress)
+
+ verify(state).listener(contractAddress)?.onSyncStateUpdate()
+ }
+
+ @Test
+ fun testOnUpdateBalance() {
+ val balance = "345"
+ kit.onUpdateBalance(balance)
+ verify(state).balance = balance
+ verify(listener).onBalanceUpdate()
+ }
+
+ @Test
+ fun testOnUpdateErc20Balance() {
+ val contractAddress = "address"
+ val balance = "345"
+
+ kit.onUpdateErc20Balance(balance, contractAddress)
+
+ verify(state).setBalance(balance, contractAddress)
+ verify(state).listener(contractAddress)?.onBalanceUpdate()
+ }
+
+ @Test
+ fun testOnUpdateTransactions() {
+ val transactions = listOf(EthereumTransaction())
+
+ kit.onUpdateTransactions(transactions)
+ verify(listener).onTransactionsUpdate(transactions)
+ }
+
+ @Test
+ fun testOnUpdateTransactions_empty() {
+ val transactions = listOf()
+
+ kit.onUpdateTransactions(transactions)
+ verify(listener, never()).onTransactionsUpdate(transactions)
+ }
+
+ @Test
+ fun testOnUpdateErc20Transactions() {
+ val contractAddress = "address"
+ val transactions = listOf(EthereumTransaction())
+
+ kit.onUpdateErc20Transactions(transactions, contractAddress)
+ verify(state).listener(contractAddress)?.onTransactionsUpdate(transactions)
+ }
+
+
+}
diff --git a/ethereumkit/src/test/java/io/horizontalsystems/ethereumkit/RxBaseTest.kt b/ethereumkit/src/test/java/io/horizontalsystems/ethereumkit/RxBaseTest.kt
new file mode 100644
index 00000000..b4318ee6
--- /dev/null
+++ b/ethereumkit/src/test/java/io/horizontalsystems/ethereumkit/RxBaseTest.kt
@@ -0,0 +1,26 @@
+package io.horizontalsystems.ethereumkit
+
+import io.reactivex.Scheduler
+import io.reactivex.android.plugins.RxAndroidPlugins
+import io.reactivex.internal.schedulers.ExecutorScheduler
+import io.reactivex.plugins.RxJavaPlugins
+import io.reactivex.schedulers.Schedulers
+import java.util.concurrent.Executor
+
+
+object RxBaseTest {
+
+ fun setup() {
+ val immediate = object : Scheduler() {
+ override fun createWorker(): Scheduler.Worker {
+ return ExecutorScheduler.ExecutorWorker(Executor { it.run() })
+ }
+ }
+ //https://medium.com/@fabioCollini/testing-asynchronous-rxjava-code-using-mockito-8ad831a16877
+ RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() }
+ RxJavaPlugins.setComputationSchedulerHandler { Schedulers.trampoline() }
+ RxJavaPlugins.setNewThreadSchedulerHandler { Schedulers.trampoline() }
+ RxAndroidPlugins.setInitMainThreadSchedulerHandler{ immediate }
+ }
+
+}
diff --git a/ethereumkit/src/test/java/io/horizontalsystems/ethereumkit/light/net/connection/ConnectionTest.kt b/ethereumkit/src/test/java/io/horizontalsystems/ethereumkit/light/net/connection/ConnectionTest.kt
new file mode 100644
index 00000000..6f5bfddc
--- /dev/null
+++ b/ethereumkit/src/test/java/io/horizontalsystems/ethereumkit/light/net/connection/ConnectionTest.kt
@@ -0,0 +1,88 @@
+package io.horizontalsystems.ethereumkit.light.net.connection
+
+import io.horizontalsystems.ethereumkit.core.hexStringToByteArray
+import io.horizontalsystems.ethereumkit.light.crypto.CryptoUtils
+import io.horizontalsystems.ethereumkit.light.crypto.ECKey
+import io.horizontalsystems.ethereumkit.light.net.IMessage
+import io.horizontalsystems.ethereumkit.light.net.Node
+import io.horizontalsystems.ethereumkit.light.net.PeerGroup
+import io.horizontalsystems.ethereumkit.light.net.Ropsten
+import io.horizontalsystems.ethereumkit.light.net.les.messages.ProofsMessage
+import org.junit.Before
+import org.junit.Test
+import java.math.BigInteger
+
+class ConnectionTest {
+
+ @Before
+ fun setUp() {
+ }
+
+
+ @Test
+ fun run() {
+ var disconnected = false
+
+ val node = Node(id = "1baf02c18c08ab0d009ccc9b51168be6a8776509ff229a6ca08507b53579cb99e0df1709bd1bcf64aed348f9a31298842cf12c1764c8de9d28abb921a548ad8c".hexStringToByteArray(),
+ host = "eth-testnet.horizontalsystems.xyz",
+ port = 20303,
+ discoveryPort = 30301)
+
+ val connection = Connection(node, object : IPeerConnectionListener {
+ override fun onConnectionEstablished() {
+ }
+
+ override fun connectionKey(): ECKey {
+ return CryptoUtils.ecKeyFromPrivate(BigInteger("38208918395832628331087730025239389699013035341486183519748173810236817397977"))
+ }
+
+ override fun onDisconnected(error: Throwable?) {
+ disconnected = true
+ }
+
+ override fun onMessageReceived(message: IMessage) {
+ }
+ })
+
+// val messagePackets = "f864b840636dbcba238de1077d89fa427f3ae59c166588d8422a1b05ea275b8ff4f9d00c86b59d656a11d9735d86538a5624b22731c026412327af7e8644c57a73f053cea084334c29edc3fe69cf73f3b35719b933e34fcf51dd6cf2916e86d9dbedd90d5b040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".hexStringToByteArray()
+// val authAckMessage = AuthAckMessage.decode(messagePackets)
+//
+// println("authAckMesage: $authAckMessage")
+ connection.connect()
+
+ while (!disconnected) {
+ println("running")
+ Thread.sleep(1000)
+ }
+
+ println("disconnected")
+ }
+
+
+ @Test
+ fun peerGroupTest() {
+ val peerGroup = PeerGroup(Ropsten(), "0x")
+
+ peerGroup.start()
+
+ Thread.sleep(120 * 1000)
+
+ }
+
+ @Test
+ fun trieNodeTest() {
+ val payload = "f90d0c882d0eed20df20d3318411de1135f90cfbf90211a0331ba6b15a470e1c9a56c23b0f1f77f8805395bc64c5f7c87eb235dfb35d61b3a005fe6f3dbd44bbdc20817b4aed358bce485ec4fb83f36f87c3130b94bd0a017ca04b7b81d76c0d1672615e411fdf4a89871f3e7117af2328ec6ab29f5a611cbc9da00f7dea9686fdfd99fe3d42c3492f5c405704d7ba5b4a18797f668ff381a17a7ca0af75a967ae62e3b804944d095a6b6f5774525394f13ef54d3751d978e55ef4f3a0bfc2dd5377e0b52a435d53fd817018f24d05346e8f86248e6f8c99d2a9efc58ea034118844292d317093a72751d4f93f549666b9e4acdab97748d892f642a2a760a019e8d1ea3c001eae2550e4abbc16f525461005e95749f10430d7b117145d7a14a075c629cf3d99916cd97f2355b030c32f21a941bc1ff94bb107b195c5091e210ca03370e5aaa3300c9fc59fe85acc1df449c9779398b40c53a54349f79a57afdca0a09b65f42cb0a37d56961b4b117178b09aaf9fb929da199263788d4157eae77810a065579366abe0207c1162ca2a11dbd1a55508df958d535b27142964a8b4392e8da009e51825e46605a84c1585519b13eacb35e7eca2f68b9d6f7fa5ce15cf7aff5ea04659aaa5dfe4984612c8a72df532913d14609f534bb138675949de4f31df7f6aa0dac82d9907fe3768ecbd50b893c847fe44c19f3766e1c05d5be2926112f9c3d8a00bbb91eae67d9505b1ab5233e81e90587b7a7772ba446083e37cd925fa3d7b3180f90211a0b88f84a4aa51250c6220fb182cfc7a68ba20f03cbd65400eec75788420db0152a036cbaa7015869e4633a9c32afbf55b5d8f098a2ef5f45a17e6409ddaec9f0376a099be02088cc445d8ab14973c6ae0486fec103b2110aca27cb15f1445b5fbc549a09e45d98390ba5497572a3aa626a21ad8c001149bdc8a8ac18bce8596516f6baca094c0c204b2de6d8f2d81385277afc3c26708707ee6c1ceab8b875bc80ff5a8ada06de84c31303a9d22808f8628694a1c7bbad6cb184bf295a1bb7db22b8f1388eca071d8829959680aae5194d130154b73438ec69c7fe120c29985d752e753702fe2a004050daac117dd99e1ea0ce5d4103ee7d23810dddb19be98f7b35a523a2a394ea06479fc93a250ce11788bd30e63755b711487d7e77ce5cd0ae3611d3af93c1117a08c6caea65888ef2309e05ded4e0f266aa741707fd8ea07855ab46935e1dcbb7ea0ef41326ab916b61f487279741bc7f72b293f4576d51afbd6342e4dfaad57710aa0ba6214c2340d8a99622a5ededd43ec359d05657470b744ff86c49fa08ecae7e8a0fd963dac4f522583a42e3e76dc09fefedda2ce03c75f2dbba046f61faf850975a0005d4dcbf21a076a3f38b0d9326380f32ad9bbb20a5a49014df26f569598c1a8a067c173d06b046edd1cf8e3961568a915f442666f80ec4a88d64985a9c23ce219a02744973b8f7367f85e471c2435b81dfe40f96a5b03c0c8d5914c41b19af030e480f90211a003eb4493a1259259019c1204f07d4f29012828994bd9d8af731c5eabfed5f5ada0edc6b9c3702d660b73df039a1d53d7a3ae8f7219501f18a545e340e269e7c6faa0342de1102d205bb0464fa10f67d55b160e2fe246fcc7158eaf7fb5c458e16396a0a21ab62e7ace187d485fb2be539c026c1bd1ad164e9101f4d983515e22d8f689a07cd3fe9db0dd1d8f60b614909110398d27b6a3e8d0be0a4d69311097c045d2afa0a984be93d5fe11eabb3154a579065719247121ddaa8c22314a43c127c891def4a05c2be5bd627aec348e95ec927bfd53f6bee3b571126219c556b5a01b65d14cd3a02b9df70212aea4562cffbeaec5b20d024a8120b6d71776db4712eef4cbcbdfd6a0b38bb21849beb661e54053fe0ee90aad619c9e3d0b2548e8008c9a86037b8727a0cab742d851891085b4893649007260faa0736d42c51abc99487c57e5dd59a535a0450560666319e815658523889b21027e1c06999520a7be212aed02664f6c80f1a086b161e4fb0ed2dc5ad4bbc65b11c864032dce0e5d03d4ad6f259a890de550dea0c0dc39a8e681fb9a76ad8e67d0798f620201d782a7892a740c892544b9eda233a0f723c7a4892be1c431d3f3faf4818793530a9522e989a964fe114cefcff085d0a0896428f94f23690a42b03cda461d15117b989b791c61da733a7bb21370930407a08903f65a5df33c58f01f0ec8e5c28acec97004d3a70f44615ebb2ba9ecbd682380f90211a037599f5e90a66f295526d8050149921d5d7ecde1a49514357ab579f7cf009657a0d527f3804a13bd75cdae98951672d2d34ba9ba9c5471400a54bae2ae743eeed1a079752d4d898f42b77a6b2d889db061a789747877cf09d46fad58db7e324bb17fa06dfba8b80879386c6dfd8e7f74ecd202306981f83cc9a235aa9726e5a015e5f7a05d4abc1521a32bd246f5c572bd9926ac31d83252f58bb226d5b54b3db3ed6acea015ff073846ad1757c2e9ba147a8e22fc1561c1f2aa8dc833ce27c451f746ce16a04a2a0e468b85f36dd835486d712777b70c63045b82a74a6e76a834cb5c119072a0e9f547cb561d5e1cbb701e1de159614a82bbba722d0f932229456bc34990eb2aa026ae58c9f4c7c81c818f0dcdf9d6e2534b1d03f78257cce2cfbe1d9d3afc4ed8a0580a4d457f80bb7a99b239de2103ed427c649c7ccbee5c18f02cc815d0986880a081647a0aee6f71d6369533055dab21f9012c5c62a9f7ea4e07b6d9d1417b3ad1a0a416d6666c6be107005afc9080b035b5e352cce94222f0f985790b348c0e923da0d5df060aa46930ee985ba57e53ef5107df8950990258fd8b02cee50c3091e7f8a078150b68816d70af6886f4e1a72130257507eab9cf5a6b3f2b2bfa76fa0677d1a0dfaa3f622e5422bfe50835ae5a79468bbcafdb8ff75942399cdca632b272eb09a0f3e9289812301290ddb887c1836afb4d28ce95a788a5dd16aa7832142120d5f180f90211a0517fdaeb626480005c269180dc48aa731d2240dc5df0cedc53b1c9a8fb7f2493a05436dda9bd1bf6af5940c24fdf2b81f20771d96b306a9d060d62db0582f37975a0c565fc5355a75ba69829d9bed27090d0c4834dd2af8a44bb491c2bee73dcbad3a0a4bf0607f5fb6d6a7f0c868bd2fb42d76c2b6881312baafa311e5ddd8abb95efa0d3c590421818cd5cf988ca81b1befaf0820399ac1ecf21af175f6b19e0d5ca55a0aac03a0a2fe76de7528b5d731ca1f1efb1a65f8909b158af926e4cf0ba10453ea021f86df2d873cd5bbbabfbddf7db042d81329621aeb127f27c9b9369c13f05e3a00216a2e6c20ff432f26b5537fd7ea9053425431b7d06d609208e6bb552714f80a0ffb83424ff24e10932e25be1a71cf1ad8223f67664e775cac754d1f4514caa45a0cf619a789866187b641f59fd4c82330b21c589826bd5649b18a67922e10f4ddba0bef9d386be60fbdf0b8643b682652bd1b49b69a2e4c451e915fa666682a56253a030811b936d31422d366903413312b2e7eb40a58314cc3eb2fa44b2516568f83ba018d65095270362c84b986525dd58b42fc325033b647893f1b0a83cae838609a4a0dfa2a0cb265c8896d05877599527a2845035d4f68b7eb5dc67738543b1d7b265a0d5b89864022e98ba9678bf12d8c8dcf8fbf9bbc9a4982c4c79e62ce1dcdc6ca0a04a6ced2c3343acc38f33dd4dd886d200399db82d2542a38ce4109e92d1163c1680f901d1a011cc6953317ec304b5551a113ef671b50e580249dfb630d5bd314c89a0fb73b0a072b98b67559dfe3b876d53751164327fac0fbc289c2f9cd1bd1a62e955726cf5a028b6c1d1086b459b47f0f393d1a7124b12830dbfb00950f5c73e234eeb596996a04f0ba60f0cf9e2dd0227e7da124adf0bdd7a7e69cc63a20753c7e947918ca430a08ce9ffbb6c7efb9c7e3fbbd34de5e48b300742d0099e4e5dcfadf1287a06736ea01c17bad40cc94714956252c8f5a96a846a633628dfd6873f737bc19edad99a61a040b174895cf64393977c95e13c594814e86c66a6966d575f8486b28b4a5fbcc1a092ffc17a391e270d5b37a93b1bb26b366de52a0af1a35b31b850fc6d7a222764a09ebb6d37a2251ece98c75a8e9c6fdd5987181334b14d932f4b1c4d93e4537a0aa032bb5ad7e3dcf3871407a06d61007e5d2effdcbbb96793a64df069d7daf20a128080a0a9bc1c71ce256ce8dcff00b3fe0bd379130d2aa7dc08f0462835541db63a5f5ba07d8e6ad81b06d856d35dce953d6edd6499434c2fb5b1ae3ad62a4a5df1f55597a047cbe69a85a23c009b46a737c8c3cb811f5af9f8f59cd5b096c204d77212e42ea0d15542bbde5ece317a29e923830684685307ddb61916faadfbe2429e1aecf1ab80f85180a0f29c510467aa617f6847fa0e00518b48845ca242b9ca5302da03deb627878c798080808080808080808080a0e2a847186ee128d12b74b7b77aeb9a0db544a1a49bcf9fec57fe04de72c62ffc808080f86e9d30681f16030cb7d2c2140183acc0538a2464ae0f49159195c86100ffa2b84ef84c5988093a1054b58f57e8a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470".hexStringToByteArray()
+
+ val proofsMessage = ProofsMessage(payload)
+
+ println("$proofsMessage")
+
+ val stateRoot = "2be24d2b03ec1ca17e3299f1d2865e7f030d431e012e3dc116b563d62e9f2f45".hexStringToByteArray()
+ val address = "f757461bdc25ee2b047d545a50768e52d530b750".hexStringToByteArray()
+ //path 0681f16030cb7d2c2140183acc0538a2464ae0f49159195c86100ffa2
+
+
+ proofsMessage.getValidatedState(stateRoot, address)
+
+ }
+}
\ No newline at end of file
diff --git a/ethereumkit/src/test/java/io/horizontalsystems/ethereumkit/light/net/connection/EncryptionHandshakeTest.kt b/ethereumkit/src/test/java/io/horizontalsystems/ethereumkit/light/net/connection/EncryptionHandshakeTest.kt
new file mode 100644
index 00000000..2b38da9f
--- /dev/null
+++ b/ethereumkit/src/test/java/io/horizontalsystems/ethereumkit/light/net/connection/EncryptionHandshakeTest.kt
@@ -0,0 +1,167 @@
+package io.horizontalsystems.ethereumkit.light.net.connection
+
+import com.nhaarman.mockito_kotlin.verify
+import com.nhaarman.mockito_kotlin.verifyNoMoreInteractions
+import com.nhaarman.mockito_kotlin.whenever
+import io.horizontalsystems.ethereumkit.light.RandomUtils
+import io.horizontalsystems.ethereumkit.light.crypto.CryptoUtils
+import io.horizontalsystems.ethereumkit.light.crypto.ECIESEncryptedMessage
+import io.horizontalsystems.ethereumkit.light.crypto.ECKey
+import io.horizontalsystems.ethereumkit.light.net.connection.messages.AuthAckMessage
+import io.horizontalsystems.ethereumkit.light.net.connection.messages.AuthMessage
+import io.horizontalsystems.ethereumkit.light.rlp.RLP
+import io.horizontalsystems.ethereumkit.light.xor
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.powermock.api.mockito.PowerMockito
+import org.powermock.core.classloader.annotations.PrepareForTest
+import org.powermock.modules.junit4.PowerMockRunner
+import org.spongycastle.crypto.digests.KeccakDigest
+import org.spongycastle.math.ec.ECPoint
+
+@RunWith(PowerMockRunner::class)
+@PrepareForTest(EncryptionHandshake::class)
+class EncryptionHandshakeTest {
+
+ private lateinit var encryptionHandshake: EncryptionHandshake
+ private lateinit var myKey: ECKey
+ private lateinit var ephemeralKey: ECKey
+ private lateinit var remoteKeyPoint: ECPoint
+ private lateinit var encodedAuthECIESMessage: ByteArray
+
+ private val cryptoUtils = Mockito.mock(CryptoUtils::class.java)
+ private val randomUtils = Mockito.mock(RandomUtils::class.java)
+ private val nonce = ByteArray(32) { 0 }
+ private val remoteNonce = ByteArray(32) { 1 }
+ private val signature = ByteArray(32) { 5 }
+ private val authECIESMessage = ECIESEncryptedMessage(ByteArray(0), ByteArray(0), ByteArray(0), ByteArray(0), ByteArray(0))
+
+ @Before
+ fun setUp() {
+ myKey = RandomUtils.randomECKey()
+ ephemeralKey = RandomUtils.randomECKey()
+ remoteKeyPoint = RandomUtils.randomECKey().publicKeyPoint
+ encodedAuthECIESMessage = authECIESMessage.encoded()
+
+ whenever(randomUtils.randomBytes(32)).thenReturn(nonce)
+ whenever(randomUtils.randomECKey()).thenReturn(ephemeralKey)
+
+ encryptionHandshake = EncryptionHandshake(myKey, remoteKeyPoint, cryptoUtils, randomUtils)
+
+ verify(randomUtils).randomECKey()
+ verify(randomUtils).randomBytes(32)
+ }
+
+ @Test
+ fun createMessage() {
+ val junkData = ByteArray(102) { 2 }
+ val sharedSecret = ByteArray(32) { 3 }
+ val authMessage = AuthMessage(signature, myKey.publicKeyPoint, nonce)
+
+ whenever(randomUtils.randomBytes(200..300)).thenReturn(junkData)
+ whenever(cryptoUtils.eciesEncrypt(remoteKeyPoint, authMessage.encoded() + junkData)).thenReturn(authECIESMessage)
+ whenever(cryptoUtils.ecdhAgree(myKey, remoteKeyPoint)).thenReturn(sharedSecret)
+ whenever(cryptoUtils.ellipticSign(sharedSecret.xor(nonce), ephemeralKey)).thenReturn(signature)
+
+ PowerMockito.whenNew(AuthMessage::class.java)
+ .withArguments(signature, myKey.publicKeyPoint, nonce)
+ .thenReturn(authMessage)
+
+ val authMessagePacket = encryptionHandshake.createAuthMessage()
+
+ verify(cryptoUtils).ecdhAgree(myKey, remoteKeyPoint)
+ verify(cryptoUtils).ellipticSign(sharedSecret.xor(nonce), ephemeralKey)
+ verify(randomUtils).randomBytes(200..300)
+ verify(cryptoUtils).eciesEncrypt(remoteKeyPoint, authMessage.encoded() + junkData)
+
+ verifyNoMoreInteractions(cryptoUtils)
+
+ Assert.assertArrayEquals(encodedAuthECIESMessage, authMessagePacket)
+ }
+
+ @Test
+ fun handleAuthAckMessage() {
+ val remoteEphemeralKeyPoint: ECPoint = RandomUtils.randomECKey().publicKeyPoint
+ val eciesMessage = ECIESEncryptedMessage(ByteArray(1), ByteArray(0), ByteArray(0), ByteArray(0), ByteArray(0))
+
+ val encodedRemoteEphemKeyPoint = remoteEphemeralKeyPoint.getEncoded(false)
+ val encodedAuthAckMessage = RLP.encodeList(
+ RLP.encodeElement(encodedRemoteEphemKeyPoint.copyOfRange(1, encodedRemoteEphemKeyPoint.size)),
+ RLP.encodeElement(remoteNonce),
+ RLP.encodeInt(4)
+ )
+
+ val authAckMessage = AuthAckMessage(encodedAuthAckMessage)
+
+ val agreedSecret = ByteArray(32) { 4 }
+ val nonceHash = ByteArray(32) { 5 }
+ val sharedSecret = ByteArray(32) { 6 }
+ val secretsAes = ByteArray(32) { 7 }
+ val secretsMac = ByteArray(32) { 8 }
+ val secretsToken = ByteArray(32) { 9 }
+
+ whenever(cryptoUtils.eciesDecrypt(myKey.privateKey, eciesMessage)).thenReturn(encodedAuthAckMessage)
+ whenever(cryptoUtils.ecdhAgree(ephemeralKey, remoteEphemeralKeyPoint)).thenReturn(agreedSecret)
+ whenever(cryptoUtils.sha3(remoteNonce + nonce)).thenReturn(nonceHash)
+ whenever(cryptoUtils.sha3(agreedSecret + nonceHash)).thenReturn(sharedSecret)
+ whenever(cryptoUtils.sha3(agreedSecret + sharedSecret)).thenReturn(secretsAes)
+ whenever(cryptoUtils.sha3(agreedSecret + secretsAes)).thenReturn(secretsMac)
+ whenever(cryptoUtils.sha3(sharedSecret)).thenReturn(secretsToken)
+
+ PowerMockito.whenNew(AuthAckMessage::class.java)
+ .withArguments(encodedAuthAckMessage)
+ .thenReturn(authAckMessage)
+
+ val secrets = encryptionHandshake.handleAuthAckMessage(eciesMessage)
+
+ verify(cryptoUtils).eciesDecrypt(myKey.privateKey, eciesMessage)
+ verify(cryptoUtils).ecdhAgree(ephemeralKey, remoteEphemeralKeyPoint)
+ verify(cryptoUtils).sha3(remoteNonce + nonce)
+ verify(cryptoUtils).sha3(agreedSecret + nonceHash)
+ verify(cryptoUtils).sha3(agreedSecret + sharedSecret)
+ verify(cryptoUtils).sha3(agreedSecret + secretsAes)
+ verify(cryptoUtils).sha3(sharedSecret)
+
+ verifyNoMoreInteractions(cryptoUtils)
+
+ val expectedEgress = KeccakDigest(256)
+ expectedEgress.update(secretsMac.xor(authAckMessage.nonce), 0, secretsMac.size)
+ expectedEgress.update(encodedAuthECIESMessage, 0, encodedAuthECIESMessage.size)
+
+ val expectedEgressMac = ByteArray(expectedEgress.digestSize)
+ KeccakDigest(expectedEgress).doFinal(expectedEgressMac, 0)
+
+ val egressMac = ByteArray(secrets.egressMac.digestSize)
+ KeccakDigest(secrets.egressMac).doFinal(egressMac, 0)
+
+ Assert.assertArrayEquals(expectedEgressMac, egressMac)
+
+ val expectedIngress = KeccakDigest(256)
+ expectedIngress.update(secretsMac.xor(nonce), 0, secretsMac.size)
+ expectedIngress.update(eciesMessage.encoded(), 0, eciesMessage.encoded().size)
+
+ val expectedIngressMac = ByteArray(expectedIngress.digestSize)
+ KeccakDigest(expectedIngress).doFinal(expectedIngressMac, 0)
+
+ val ingressMac = ByteArray(secrets.ingressMac.digestSize)
+ KeccakDigest(secrets.ingressMac).doFinal(ingressMac, 0)
+
+ Assert.assertArrayEquals(expectedIngressMac, ingressMac)
+ }
+
+ @Test(expected = EncryptionHandshake.HandshakeError.InvalidAuthAckPayload::class)
+ fun invalidAuthAckPayload() {
+ val eciesMessage = ECIESEncryptedMessage(ByteArray(1), ByteArray(0), ByteArray(0), ByteArray(0), ByteArray(0))
+ val encodedAuthAckMessage = ByteArray(0)
+ whenever(cryptoUtils.eciesDecrypt(myKey.privateKey, eciesMessage)).thenReturn(encodedAuthAckMessage)
+
+ PowerMockito.whenNew(AuthAckMessage::class.java)
+ .withArguments(encodedAuthAckMessage)
+ .thenThrow(Exception())
+
+ encryptionHandshake.handleAuthAckMessage(eciesMessage)
+ }
+}
\ No newline at end of file
diff --git a/ethereumkit/src/test/java/io/horizontalsystems/ethereumkit/light/net/connection/FrameCodecTest.kt b/ethereumkit/src/test/java/io/horizontalsystems/ethereumkit/light/net/connection/FrameCodecTest.kt
new file mode 100644
index 00000000..834a8b37
--- /dev/null
+++ b/ethereumkit/src/test/java/io/horizontalsystems/ethereumkit/light/net/connection/FrameCodecTest.kt
@@ -0,0 +1,272 @@
+package io.horizontalsystems.ethereumkit.light.net.connection
+
+import com.nhaarman.mockito_kotlin.verify
+import com.nhaarman.mockito_kotlin.verifyNoMoreInteractions
+import com.nhaarman.mockito_kotlin.whenever
+import io.horizontalsystems.ethereumkit.light.rlp.RLP
+import org.junit.Assert.*
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito
+import org.spongycastle.crypto.digests.KeccakDigest
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+
+class FrameCodecTest {
+
+ private lateinit var frameCodec: FrameCodec
+ private lateinit var secrets: Secrets
+
+ private val frameCodecHelper = Mockito.mock(FrameCodecHelper::class.java)
+ private val aesEncryptor = Mockito.mock(AESCipher::class.java)
+ private val aesDecryptor = Mockito.mock(AESCipher::class.java)
+
+ private val encryptedHeader = ByteArray(16) { 3 }
+ private val encryptedBody = ByteArray(16) { 4 }
+ private val headerMac = ByteArray(16) { 5 }
+ private val bodyMac = ByteArray(16) { 6 }
+ private val updatedEgressDigest = ByteArray(KeccakDigest().digestSize)
+
+ @Before
+ fun setUp() {
+ secrets = Secrets(
+ aes = ByteArray(16) { 0 },
+ mac = ByteArray(16) { 0 },
+ token = ByteArray(32) { 2 },
+ egressMac = KeccakDigest(),
+ ingressMac = KeccakDigest())
+
+ val egress = KeccakDigest()
+ egress.update(encryptedBody, 0, encryptedBody.size)
+ egress.doFinal(updatedEgressDigest, 0)
+
+ whenever(frameCodecHelper.updateMac(secrets.egressMac, secrets.mac, encryptedHeader)).thenReturn(headerMac)
+ whenever(frameCodecHelper.updateMac(secrets.egressMac, secrets.mac, updatedEgressDigest)).thenReturn(bodyMac)
+ whenever(frameCodecHelper.updateMac(secrets.ingressMac, secrets.mac, encryptedHeader)).thenReturn(headerMac)
+ whenever(frameCodecHelper.updateMac(secrets.ingressMac, secrets.mac, updatedEgressDigest)).thenReturn(bodyMac)
+
+ frameCodec = FrameCodec(secrets, frameCodecHelper, aesEncryptor, aesDecryptor)
+ }
+
+ @Test
+ fun writeFrame() {
+ val frame = Frame(0, ByteArray(15) { 10 })
+
+ val frameSizeBytes = ByteArray(3) { 0 }
+ val header = frameSizeBytes + RLP.encodeList(RLP.encodeInt(0)) + ByteArray(11) { 0 }
+ val body = RLP.encodeInt(frame.type) + frame.payload
+
+ whenever(frameCodecHelper.toThreeBytes(frame.size + 1)).thenReturn(frameSizeBytes)
+ whenever(aesEncryptor.process(header)).thenReturn(encryptedHeader)
+ whenever(aesEncryptor.process(body)).thenReturn(encryptedBody)
+
+ val outputStream = ByteArrayOutputStream()
+ frameCodec.writeFrame(frame, outputStream)
+
+
+ verify(aesEncryptor).process(header)
+ verify(aesEncryptor).process(body)
+ verify(frameCodecHelper).toThreeBytes(frame.size + 1)
+ verify(frameCodecHelper).updateMac(secrets.egressMac, secrets.mac, encryptedHeader)
+ verify(frameCodecHelper).updateMac(secrets.egressMac, secrets.mac, updatedEgressDigest)
+
+ verifyNoMoreInteractions(aesEncryptor)
+ verifyNoMoreInteractions(aesDecryptor)
+ verifyNoMoreInteractions(frameCodecHelper)
+
+ assertArrayEquals(encryptedHeader + headerMac + encryptedBody + bodyMac, outputStream.toByteArray())
+ }
+
+ @Test
+ fun writeFrame_contextId() {
+ val frame = Frame(0, ByteArray(15) { 10 }, 1, 1)
+ val frameSizeBytes = ByteArray(3) { 0 }
+ val headerInfo = frameSizeBytes + RLP.encodeList(RLP.encodeInt(0), RLP.encodeInt(1), RLP.encodeInt(1))
+ val headerPadding = ByteArray(16 - headerInfo.size) { 0 }
+
+ val header = headerInfo + headerPadding
+ val body = RLP.encodeInt(frame.type) + frame.payload
+
+ whenever(frameCodecHelper.toThreeBytes(frame.size + 1)).thenReturn(frameSizeBytes)
+ whenever(aesEncryptor.process(header)).thenReturn(encryptedHeader)
+ whenever(aesEncryptor.process(body)).thenReturn(encryptedBody)
+
+ val outputStream = ByteArrayOutputStream()
+ frameCodec.writeFrame(frame, outputStream)
+
+ verify(aesEncryptor).process(header)
+ verify(aesEncryptor).process(body)
+ verify(frameCodecHelper).toThreeBytes(frame.size + 1)
+ verify(frameCodecHelper).updateMac(secrets.egressMac, secrets.mac, encryptedHeader)
+ verify(frameCodecHelper).updateMac(secrets.egressMac, secrets.mac, updatedEgressDigest)
+
+ verifyNoMoreInteractions(aesEncryptor)
+ verifyNoMoreInteractions(aesDecryptor)
+ verifyNoMoreInteractions(frameCodecHelper)
+
+ assertArrayEquals(encryptedHeader + headerMac + encryptedBody + bodyMac, outputStream.toByteArray())
+ }
+
+ @Test
+ fun writeFrame_framePadding() {
+ val frame = Frame(0, ByteArray(16) { 10 })
+
+ val frameSizeBytes = ByteArray(3) { 0 }
+ val header = frameSizeBytes + RLP.encodeList(RLP.encodeInt(0)) + ByteArray(11) { 0 }
+ val body = RLP.encodeInt(frame.type) + frame.payload + ByteArray(15) { 0 }
+
+ whenever(frameCodecHelper.toThreeBytes(frame.size + 1)).thenReturn(frameSizeBytes)
+ whenever(aesEncryptor.process(header)).thenReturn(encryptedHeader)
+ whenever(aesEncryptor.process(body)).thenReturn(encryptedBody)
+
+ val outputStream = ByteArrayOutputStream()
+ frameCodec.writeFrame(frame, outputStream)
+
+ verify(aesEncryptor).process(header)
+ verify(aesEncryptor).process(body)
+ verify(frameCodecHelper).toThreeBytes(frame.size + 1)
+ verify(frameCodecHelper).updateMac(secrets.egressMac, secrets.mac, encryptedHeader)
+ verify(frameCodecHelper).updateMac(secrets.egressMac, secrets.mac, updatedEgressDigest)
+
+ verifyNoMoreInteractions(aesEncryptor)
+ verifyNoMoreInteractions(aesDecryptor)
+ verifyNoMoreInteractions(frameCodecHelper)
+
+ assertArrayEquals(encryptedHeader + headerMac + encryptedBody + bodyMac, outputStream.toByteArray())
+ }
+
+ @Test
+ fun readFrame() {
+ val frame = Frame(0, ByteArray(15) { 10 })
+
+ val frameSizeBytes = ByteArray(3) { 0 }
+ val header = frameSizeBytes + RLP.encodeList(RLP.encodeInt(0)) + ByteArray(11) { 0 }
+ val body = RLP.encodeInt(frame.type) + frame.payload
+
+ whenever(frameCodecHelper.fromThreeBytes(frameSizeBytes)).thenReturn(frame.size + 1)
+ whenever(aesDecryptor.process(encryptedHeader)).thenReturn(header)
+ whenever(aesDecryptor.process(encryptedBody)).thenReturn(body)
+
+ val inputStream = ByteArrayInputStream(encryptedHeader + headerMac + encryptedBody + bodyMac)
+
+ val result = frameCodec.readFrame(inputStream)
+
+ verify(aesDecryptor).process(encryptedHeader)
+ verify(aesDecryptor).process(encryptedBody)
+ verify(frameCodecHelper).fromThreeBytes(frameSizeBytes)
+ verify(frameCodecHelper).updateMac(secrets.ingressMac, secrets.mac, encryptedHeader)
+ verify(frameCodecHelper).updateMac(secrets.ingressMac, secrets.mac, updatedEgressDigest)
+
+ verifyNoMoreInteractions(aesEncryptor)
+ verifyNoMoreInteractions(aesDecryptor)
+ verifyNoMoreInteractions(frameCodecHelper)
+
+ assertNotNull(result)
+ assertEquals(result!!.type, frame.type)
+ assertArrayEquals(result.payload, frame.payload)
+ assertEquals(result.size, frame.size)
+ assertEquals(result.contextId, frame.contextId)
+ assertEquals(result.totalFrameSize, frame.totalFrameSize)
+ }
+
+ @Test
+ fun readFrame_contextId() {
+ val frame = Frame(0, ByteArray(15) { 10 }, 1, 1)
+ val frameSizeBytes = ByteArray(3) { 0 }
+ val headerInfo = frameSizeBytes + RLP.encodeList(RLP.encodeInt(0), RLP.encodeInt(1), RLP.encodeInt(1))
+ val headerPadding = ByteArray(16 - headerInfo.size) { 0 }
+
+ val header = headerInfo + headerPadding
+ val body = RLP.encodeInt(frame.type) + frame.payload
+
+ whenever(frameCodecHelper.fromThreeBytes(frameSizeBytes)).thenReturn(frame.size + 1)
+ whenever(aesDecryptor.process(encryptedHeader)).thenReturn(header)
+ whenever(aesDecryptor.process(encryptedBody)).thenReturn(body)
+
+ val inputStream = ByteArrayInputStream(encryptedHeader + headerMac + encryptedBody + bodyMac)
+
+ val result = frameCodec.readFrame(inputStream)
+
+ verify(aesDecryptor).process(encryptedHeader)
+ verify(aesDecryptor).process(encryptedBody)
+ verify(frameCodecHelper).fromThreeBytes(frameSizeBytes)
+ verify(frameCodecHelper).updateMac(secrets.ingressMac, secrets.mac, encryptedHeader)
+ verify(frameCodecHelper).updateMac(secrets.ingressMac, secrets.mac, updatedEgressDigest)
+
+ verifyNoMoreInteractions(aesEncryptor)
+ verifyNoMoreInteractions(aesDecryptor)
+ verifyNoMoreInteractions(frameCodecHelper)
+
+ assertNotNull(result)
+ assertEquals(result!!.type, frame.type)
+ assertArrayEquals(result.payload, frame.payload)
+ assertEquals(result.size, frame.size)
+ assertEquals(result.contextId, frame.contextId)
+ assertEquals(result.totalFrameSize, frame.totalFrameSize)
+ }
+
+ @Test
+ fun readFrame_bodyPadding() {
+ val frame = Frame(0, ByteArray(16) { 10 })
+ val frameSizeBytes = ByteArray(3) { 0 }
+ val header = frameSizeBytes + RLP.encodeList(RLP.encodeInt(0)) + ByteArray(11) { 0 }
+
+ val body = RLP.encodeInt(frame.type) + frame.payload
+
+ val encryptedBody = this.encryptedBody + ByteArray(16) { 0 }
+ val egress = KeccakDigest()
+ egress.update(encryptedBody, 0, encryptedBody.size)
+ egress.doFinal(updatedEgressDigest, 0)
+
+ whenever(frameCodecHelper.updateMac(secrets.ingressMac, secrets.mac, updatedEgressDigest)).thenReturn(bodyMac)
+
+ whenever(frameCodecHelper.fromThreeBytes(frameSizeBytes)).thenReturn(frame.size + 1)
+ whenever(aesDecryptor.process(encryptedHeader)).thenReturn(header)
+ whenever(aesDecryptor.process(encryptedBody)).thenReturn(body)
+
+ val inputStream = ByteArrayInputStream(encryptedHeader + headerMac + encryptedBody + bodyMac)
+
+ val result = frameCodec.readFrame(inputStream)
+
+ verify(aesDecryptor).process(encryptedHeader)
+ verify(aesDecryptor).process(encryptedBody)
+ verify(frameCodecHelper).fromThreeBytes(frameSizeBytes)
+ verify(frameCodecHelper).updateMac(secrets.ingressMac, secrets.mac, encryptedHeader)
+ verify(frameCodecHelper).updateMac(secrets.ingressMac, secrets.mac, updatedEgressDigest)
+
+ verifyNoMoreInteractions(aesEncryptor)
+ verifyNoMoreInteractions(aesDecryptor)
+ verifyNoMoreInteractions(frameCodecHelper)
+
+ assertNotNull(result)
+ assertEquals(result!!.type, frame.type)
+ assertArrayEquals(result.payload, frame.payload)
+ assertEquals(result.size, frame.size)
+ assertEquals(result.contextId, frame.contextId)
+ assertEquals(result.totalFrameSize, frame.totalFrameSize)
+ }
+
+ @Test(expected = FrameCodec.FrameCodecError.HeaderMacMismatch::class)
+ fun readFrame_headerMacMismatch() {
+ val inputStream = ByteArrayInputStream(encryptedHeader + ByteArray(16) { 11 } + encryptedBody + bodyMac)
+
+ frameCodec.readFrame(inputStream)
+ }
+
+ @Test(expected = FrameCodec.FrameCodecError.BodyMacMismatch::class)
+ fun readFrame_bodyMacMismatch() {
+ val frame = Frame(0, ByteArray(15) { 10 })
+
+ val frameSizeBytes = ByteArray(3) { 0 }
+ val header = frameSizeBytes + RLP.encodeList(RLP.encodeInt(0)) + ByteArray(11) { 0 }
+ val body = RLP.encodeInt(frame.type) + frame.payload
+
+ whenever(frameCodecHelper.fromThreeBytes(frameSizeBytes)).thenReturn(frame.size + 1)
+ whenever(aesDecryptor.process(encryptedHeader)).thenReturn(header)
+ whenever(aesDecryptor.process(encryptedBody)).thenReturn(body)
+
+ val inputStream = ByteArrayInputStream(encryptedHeader + headerMac + encryptedBody + ByteArray(16) { 11 })
+
+ frameCodec.readFrame(inputStream)
+ }
+}
\ No newline at end of file
diff --git a/ethereumkit/src/test/java/io/horizontalsystems/ethereumkit/light/net/connection/FrameHandlerTest.kt b/ethereumkit/src/test/java/io/horizontalsystems/ethereumkit/light/net/connection/FrameHandlerTest.kt
new file mode 100644
index 00000000..06f0f201
--- /dev/null
+++ b/ethereumkit/src/test/java/io/horizontalsystems/ethereumkit/light/net/connection/FrameHandlerTest.kt
@@ -0,0 +1,99 @@
+package io.horizontalsystems.ethereumkit.light.net.connection
+
+import io.horizontalsystems.ethereumkit.light.net.devp2p.Capability
+import io.horizontalsystems.ethereumkit.light.net.devp2p.messages.HelloMessage
+import io.horizontalsystems.ethereumkit.light.net.devp2p.messages.PingMessage
+import io.horizontalsystems.ethereumkit.light.net.les.messages.StatusMessage
+import org.junit.Assert.*
+import org.junit.Before
+import org.junit.Test
+import java.math.BigInteger
+
+class FrameHandlerTest {
+
+ private lateinit var frameHandler: FrameHandler
+
+ @Before
+ fun setUp() {
+ frameHandler = FrameHandler()
+ }
+
+ @Test
+ fun getMessage() {
+
+ val message = HelloMessage(peerId = ByteArray(64) { 0 }, port = 0, capabilities = listOf())
+
+ frameHandler.addFrame(Frame(type = 0, payload = message.encoded()))
+
+ val resolvedMessage = frameHandler.getMessage()
+
+ assertNotNull(resolvedMessage)
+ assertEquals(message::class, resolvedMessage!!::class)
+ assertArrayEquals(message.encoded(), resolvedMessage.encoded())
+ }
+
+ @Test
+ fun getMessage_EmptyFrames() {
+ val resolvedMessage = frameHandler.getMessage()
+
+ assertNull(resolvedMessage)
+ }
+
+ @Test(expected = FrameHandler.FrameHandlerError.UnknownMessageType::class)
+ fun getMessage_UnknownMessageType() {
+ val message = HelloMessage(peerId = ByteArray(64) { 0 }, port = 0, capabilities = listOf())
+
+ frameHandler.addFrame(Frame(type = 5, payload = message.encoded()))
+
+ frameHandler.getMessage()
+ }
+
+ @Test(expected = FrameHandler.FrameHandlerError.InvalidPayload::class)
+ fun getMessage_InvalidPayload() {
+ val message = PingMessage()
+
+ frameHandler.addFrame(Frame(type = 0, payload = message.encoded()))
+
+ frameHandler.getMessage()
+ }
+
+ @Test
+ fun getMessage_TwoMessagesInFrames() {
+ val helloMessage = HelloMessage(peerId = ByteArray(64) { 0 }, port = 0, capabilities = listOf())
+ val pingMessage = PingMessage()
+
+ frameHandler.addFrame(Frame(type = helloMessage.code, payload = helloMessage.encoded()))
+ frameHandler.addFrame(Frame(type = pingMessage.code, payload = pingMessage.encoded()))
+
+
+ val firstMessage = frameHandler.getMessage()
+ val secondMessage = frameHandler.getMessage()
+
+
+ assertEquals(helloMessage::class, firstMessage!!::class)
+ assertEquals(pingMessage::class, secondMessage!!::class)
+
+ assertArrayEquals(helloMessage.encoded(), firstMessage.encoded())
+ assertArrayEquals(pingMessage.encoded(), secondMessage.encoded())
+ }
+
+ @Test
+ fun addLesCapability() {
+ val helloMessage = HelloMessage(peerId = ByteArray(64) { 0 }, port = 0, capabilities = listOf())
+ val statusMessage = StatusMessage(2, 3, ByteArray(0), ByteArray(0), ByteArray(0), BigInteger.valueOf(0))
+
+ frameHandler.addCapabilities(listOf(Capability("les", 2)))
+
+ frameHandler.addFrame(Frame(type = helloMessage.code, payload = helloMessage.encoded()))
+ frameHandler.addFrame(Frame(type = statusMessage.code + 0x10, payload = statusMessage.encoded()))
+
+ val firstMessage = frameHandler.getMessage()
+ val secondMessage = frameHandler.getMessage()
+
+ assertEquals(helloMessage::class, firstMessage!!::class)
+ assertEquals(statusMessage::class, secondMessage!!::class)
+
+ assertArrayEquals(helloMessage.encoded(), firstMessage.encoded())
+ assertArrayEquals(statusMessage.encoded(), secondMessage.encoded())
+ }
+}
\ No newline at end of file
diff --git a/ethereumkit/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/ethereumkit/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 00000000..ca6ee9ce
--- /dev/null
+++ b/ethereumkit/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 9a4163a4..2d976613 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,6 @@
+#Thu Feb 14 11:48:51 KGT 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip