Add Trie datastructure, utility classes and leveldb dependency

This commit is contained in:
nicksavers 2014-05-05 04:10:38 +02:00
parent 6fca92b62f
commit 01a9fcc7e2
21 changed files with 2213 additions and 8 deletions

View File

@ -35,6 +35,18 @@
<artifactId>core</artifactId>
<version>${spongycastle.version}</version>
</dependency>
<dependency>
<groupId>org.iq80.leveldb</groupId>
<artifactId>leveldb</artifactId>
<version>${leveldb.version}</version>
</dependency>
<!-- Added Cedarsoft for DeepEqual function -->
<dependency>
<groupId>com.cedarsoftware</groupId>
<artifactId>java-util</artifactId>
<version>1.8.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fifesoft</groupId>
<artifactId>rsyntaxtextarea</artifactId>

View File

@ -24,7 +24,6 @@ import java.util.TimerTask;
import org.ethereum.gui.PeerListener;
import org.ethereum.manager.MainData;
import org.ethereum.net.Command;
import org.ethereum.net.RLP;
import org.ethereum.net.message.BlocksMessage;
import org.ethereum.net.message.DisconnectMessage;
import org.ethereum.net.message.GetChainMessage;
@ -34,6 +33,7 @@ import org.ethereum.net.message.NotInChainMessage;
import org.ethereum.net.message.PeersMessage;
import org.ethereum.net.message.StaticMessages;
import org.ethereum.net.message.TransactionsMessage;
import org.ethereum.net.rlp.RLP;
import org.ethereum.net.rlp.RLPList;
import org.ethereum.net.vo.Block;
import org.ethereum.util.Utils;

View File

@ -1,8 +1,10 @@
package org.ethereum.net.message;
import org.spongycastle.util.encoders.Hex;
import static org.ethereum.net.Command.HELLO;
import org.ethereum.net.RLP;
import org.ethereum.net.rlp.RLP;
import org.ethereum.net.rlp.RLPItem;
import org.ethereum.net.rlp.RLPList;

View File

@ -1,4 +1,4 @@
package org.ethereum.net;
package org.ethereum.net.rlp;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
@ -8,9 +8,6 @@ import java.util.Arrays;
import java.util.List;
import java.util.Queue;
import org.ethereum.net.rlp.RLPItem;
import org.ethereum.net.rlp.RLPList;
public class RLP {
public static byte decodeOneByteItem(byte[] data, int index) {

View File

@ -0,0 +1,117 @@
package org.ethereum.trie;
import static org.iq80.leveldb.impl.Iq80DBFactory.factory;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.ethereum.crypto.HashUtil;
import org.ethereum.util.Value;
import org.iq80.leveldb.DB;
import org.iq80.leveldb.Options;
import org.spongycastle.util.encoders.Hex;
public class Cache {
private Map<byte[], Node> nodes;
private DB db;
private boolean isDirty;
public Cache(DB db) {
if(db == null) {
try {
/* **** Experimental LevelDB Code **** */
Options options = new Options();
options.createIfMissing(true);
this.db = factory.open(new File("ethereumdb"), options);
/* **** Experimental LevelDB Code **** */
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
this.db = db;
nodes = new HashMap<byte[], Node>();
}
public Object put(Object o) {
Value value = new Value(o);
byte[] enc = value.encode();
if (enc.length >= 32) {
byte[] sha = Hex.encode(HashUtil.sha3(enc));
this.nodes.put(sha, new Node(sha, value, true));
this.isDirty = true;
return sha;
}
return value;
}
public Value get(byte[] key) {
// First check if the key is the cache
if (this.nodes.get(key) != null) {
return this.nodes.get(key).getValue();
}
// Get the key of the database instead and cache it
byte[] data = this.db.get(key);
Value value = new Value(data);
// Create caching node
this.nodes.put(key, new Node(key, value, false));
return value;
}
public void delete(byte[] key) {
this.nodes.remove(key);
this.db.delete(key);
}
public void commit() {
// Don't try to commit if it isn't dirty
if (!this.isDirty) {
return;
}
for (byte[] key : this.nodes.keySet()) {
Node node = this.nodes.get(key);
if (node.isDirty()) {
this.db.put(key, node.getValue().encode());
node.setDirty(false);
}
}
this.isDirty = false;
// If the nodes grows beyond the 200 entries we simple empty it
// FIXME come up with something better
if (this.nodes.size() > 200) {
this.nodes = new HashMap<byte[], Node>();
}
}
public void undo() {
Iterator<Map.Entry<byte[], Node>> iter = this.nodes.entrySet().iterator();
while (iter.hasNext()) {
if(iter.next().getValue().isDirty()){
iter.remove();
}
}
this.isDirty = false;
}
public boolean isDirty() {
return isDirty;
}
public void setDirty(boolean isDirty) {
this.isDirty = isDirty;
}
public Map<byte[], Node> getNodes() {
return nodes;
}
public DB getDb() {
return db;
}
}

View File

@ -0,0 +1,80 @@
package org.ethereum.trie;
import org.ethereum.util.Value;
/**
* A Node in a Merkle Patricia Tree is one of the following:
*
* - NULL (represented as the empty string)
* - A two-item array [ key, value ]
* - A 17-item array [ v0 ... v15, vt ]
*
* The idea is that in the event that there is a long path of nodes
* each with only one element, we shortcut the descent by setting up
* a [ key, value ] node, where the key gives the hexadecimal path
* to descend, in the compact encoding described above, and the value
* is just the hash of the node like in the standard radix tree.
*
* R
* / \
* / \
* N N
* / \ / \
* L L L L
*
*
* Also, we add another conceptual change: internal nodes can no longer
* have values, only leaves with no children of their own can; however,
* since to be fully generic we want the key/value store to be able to
* store keys like 'dog' and 'doge' at the same time, we simply add
* a terminator symbol (16) to the alphabet so there is never a value
* "en-route" to another value. Where a node is referenced inside a node,
* what is included is H(rlp.encode(x)) where H(x) = sha3(x) if len(x) >= 32 else x
* and rlp.encode is the RLP encoding function.
*
* Note that when updating a trie, you will need to store the key/value pair (sha3(x), x)
* in a persistent lookup table when you create a node with length >= 32,
* but if the node is shorter than that then you do not need to store anything
* when length < 32 for the obvious reason that the function f(x) = x is reversible.
*
*/
public class Node {
private byte[] key; // 1 key for 2-item array, 16 keys for 17-item array
private Value value;
private boolean dirty;
public Node(byte[] key, Value val) {
this(key, val, false);
}
public Node(byte[] key, Value val, boolean dirty) {
this.key = key;
this.value = val;
this.dirty = dirty;
}
public Node copy() {
return new Node(this.key.clone(), this.value, this.dirty);
}
public boolean isDirty() {
return dirty;
}
public void setDirty(boolean ditry) {
this.dirty = ditry;
}
public byte[] getKey() {
return key;
}
public Value getValue() {
return value;
}
public Object[] getItems() {
return new Object[] { key, value };
}
}

View File

@ -0,0 +1,368 @@
package org.ethereum.trie;
import static java.util.Arrays.copyOfRange;
import static org.spongycastle.util.Arrays.concatenate;
import java.util.Arrays;
import org.ethereum.util.CompactEncoder;
import org.ethereum.util.Value;
import org.iq80.leveldb.DB;
import com.cedarsoftware.util.DeepEquals;
public class Trie {
private static byte PAIR_SIZE = 2;
private static byte LIST_SIZE = 17;
// A (modified) Radix Trie implementation. The Trie implements
// a caching mechanism and will used cached values if they are
// present. If a node is not present in the cache it will try to
// fetch it from the database and store the cached value.
// Please note that the data isn't persisted unless `Sync` is
// explicitly called.
private Object prevRoot;
private Object root;
private Cache cache;
public Trie(DB db) {
this(db, "");
}
public Trie(DB db, Object root) {
this.cache = new Cache(db);
this.root = root;
this.prevRoot = root;
}
public TrieIterator getIterator() {
return new TrieIterator(this);
}
public Cache getCache() {
return this.cache;
}
public Object getPrevRoot() {
return prevRoot;
}
public Object getRoot() {
return root;
}
public void setRoot(Node root) {
this.root = root;
}
public void setCache(Cache cache) {
this.cache = cache;
}
/**************************************
* Public (query) interface functions *
**************************************/
/**
* Insert key/value pair into trie
*
* @param key
* @param value
*/
public void update(String key, String value) {
if (key == null)
throw new NullPointerException("Key should not be blank");
byte[] k = CompactEncoder.hexDecode(key.getBytes());
this.root = this.insertOrDelete(this.root, k, value.getBytes());
}
/**
* Retrieve a value from a node
*
* @param key
* @return value
*/
public String get(String key) {
byte[] k = CompactEncoder.hexDecode(key.getBytes());
Value c = new Value( this.get(this.root, k) );
return c.asString();
}
/**
* Delete a key/value pair from the trie
*
* @param key
*/
public void delete(String key) {
this.update(key, "");
}
/****************************************
* Private functions *
****************************************/
private Object get(Object node, byte[] key) {
// Return the node if key is empty (= found)
if (key.length == 0 || isEmptyNode(node)) {
return node;
}
Value currentNode = this.getNode(node);
int length = currentNode.length();
if (length == PAIR_SIZE) {
// Decode the key
byte[] k = CompactEncoder.decode(currentNode.get(0).asBytes());
Object v = currentNode.get(1).asObj();
if (key.length >= k.length && Arrays.equals(k, copyOfRange(key, 0, k.length))) {
return this.get(v, copyOfRange(key, k.length, key.length));
} else {
return "";
}
} else if (length == LIST_SIZE) {
return this.get(currentNode.get(key[0]).asObj(), copyOfRange(key, 1, key.length));
}
// It shouldn't come this far
throw new RuntimeException("Unexpected Node length: " + length);
}
private Object insertOrDelete(Object node, byte[] key, byte[] value) {
if (value.length != 0) {
return this.insert(node, key, value);
} else {
return this.delete(node, key);
}
}
/**
* Update or add the item inside a node
* return the updated node with rlp encoded
*/
private Object insert(Object node, byte[] key, Object value) {
if (key.length == 0) {
return value;
}
if (isEmptyNode(node)) {
Object[] newNode = new Object[] { CompactEncoder.encode(key), value };
return this.put(newNode);
}
Value currentNode = this.getNode(node);
// Check for "special" 2 slice type node
if (currentNode.length() == PAIR_SIZE) {
// Decode the key
byte[] k = CompactEncoder.decode(currentNode.get(0).asBytes());
Object v = currentNode.get(1).asObj();
// Matching key pair (ie. there's already an object with this key)
if (Arrays.equals(k, key)) {
Object[] newNode = new Object[] {CompactEncoder.encode(key), value};
return this.put(newNode);
}
Object newHash;
int matchingLength = matchingNibbleLength(key, k);
if (matchingLength == k.length) {
// Insert the hash, creating a new node
newHash = this.insert(v, copyOfRange(key, matchingLength, key.length), value);
} else {
// Expand the 2 length slice to a 17 length slice
Object oldNode = this.insert("", copyOfRange(k, matchingLength+1, k.length), v);
Object newNode = this.insert("", copyOfRange(key, matchingLength+1, key.length), value);
// Create an expanded slice
Object[] scaledSlice = emptyStringSlice(17);
// Set the copied and new node
scaledSlice[k[matchingLength]] = oldNode;
scaledSlice[key[matchingLength]] = newNode;
newHash = this.put(scaledSlice);
}
if (matchingLength == 0) {
// End of the chain, return
return newHash;
} else {
Object[] newNode = new Object[] {CompactEncoder.encode(copyOfRange(key, 0, matchingLength)), newHash};
return this.put(newNode);
}
} else {
// Copy the current node over to the new node
Object[] newNode = copyNode(currentNode);
// Replace the first nibble in the key
newNode[key[0]] = this.insert(currentNode.get(key[0]).asObj(), copyOfRange(key, 1, key.length), value);
return this.put(newNode);
}
}
private Object delete(Object node, byte[] key) {
if (key.length == 0 || isEmptyNode(node)) {
return "";
}
// New node
Value currentNode = this.getNode(node);
// Check for "special" 2 slice type node
if (currentNode.length() == PAIR_SIZE) {
// Decode the key
byte[] k = CompactEncoder.decode(currentNode.get(0).asBytes());
Object v = currentNode.get(1).asObj();
// Matching key pair (ie. there's already an object with this key)
if (Arrays.equals(k, key)) {
return "";
} else if (Arrays.equals(copyOfRange(key, 0, k.length), k)) {
Object hash = this.delete(v, copyOfRange(key, k.length, key.length));
Value child = this.getNode(hash);
Object newNode;
if (child.length() == PAIR_SIZE) {
byte[] newKey = concatenate(k, CompactEncoder.decode(child.get(0).asBytes()));
newNode = new Object[] {CompactEncoder.encode(newKey), child.get(1).asObj()};
} else {
newNode = new Object[] {currentNode.get(0).asString(), hash};
}
return this.put(newNode);
} else {
return node;
}
} else {
// Copy the current node over to a new node
Object[] itemList = copyNode(currentNode);
// Replace the first nibble in the key
itemList[key[0]] = this.delete(itemList[key[0]], copyOfRange(key, 1, key.length));
byte amount = -1;
for (byte i = 0; i < LIST_SIZE; i++) {
if (itemList[i] != "") {
if (amount == -1) {
amount = i;
} else {
amount = -2;
}
}
}
Object[] newNode = null;
if (amount == 16) {
newNode = new Object[] { CompactEncoder.encode(new byte[] {16} ), itemList[amount]};
} else if (amount >= 0) {
Value child = this.getNode(itemList[amount]);
if (child.length() == PAIR_SIZE) {
key = concatenate(new byte[]{amount}, CompactEncoder.decode(child.get(0).asBytes()));
newNode = new Object[] {CompactEncoder.encode(key), child.get(1).asObj()};
} else if (child.length() == LIST_SIZE) {
newNode = new Object[] { CompactEncoder.encode(new byte[]{amount}), itemList[amount]};
}
} else {
newNode = itemList;
}
return this.put(newNode);
}
}
/**
* Helper method to retrieve the actual node
* If the node is not a list and length is > 32 bytes get the actual node from the db
*
* @param node
* @return
*/
private Value getNode(Object node) {
Value n = new Value(node);
if (!n.get(0).isNull()) {
return n;
}
String str = n.asString();
if (str.length() == 0) {
return n;
} else if (str.length() < 32) {
return new Value(str.getBytes());
}
return this.cache.get(n.asBytes());
}
private Object put(Object node) {
/* TODO?
c := Conv(t.Root)
fmt.Println(c.Type(), c.Length())
if c.Type() == reflect.String && c.AsString() == "" {
return enc
}
*/
return this.cache.put(node);
}
private boolean isEmptyNode(Object node) {
Value n = new Value(node);
return (node == null || (n.isString() && (n.asString() == "" || n.get(0).isNull())) || n.length() == 0);
}
private Object[] copyNode(Value currentNode) {
Object[] itemList = emptyStringSlice(LIST_SIZE);
for (int i = 0; i < LIST_SIZE; i++) {
Object cpy = currentNode.get(i).asObj();
if (cpy != null) {
itemList[i] = cpy;
}
}
return itemList;
}
// Simple compare function which creates a rlp value out of the evaluated objects
public boolean cmp(Trie trie) {
return DeepEquals.deepEquals(this.root, trie.getRoot());
}
// Save the cached value to the database.
public void sync() {
this.cache.commit();
this.prevRoot = this.root;
}
public void undo() {
this.cache.undo();
this.root = this.prevRoot;
}
// Returns a copy of this trie
public Trie copy() {
Trie trie = new Trie(this.cache.getDb(), this.root);
for (byte[] key : this.cache.getNodes().keySet()) {
Node node = this.cache.getNodes().get(key);
trie.cache.getNodes().put(key, node.copy());
}
return trie;
}
/********************************
* Utility functions *
*******************************/
// Returns the amount of nibbles that match each other from 0 ...
private int matchingNibbleLength(byte[] a, byte[] b) {
int i = 0;
while (Arrays.equals(copyOfRange(a, 0, i+1), copyOfRange(b, 0, i+1)) && i < b.length) {
i++;
}
return i;
}
private Object[] emptyStringSlice(int l) {
Object[] slice = new Object[l];
for (int i = 0; i < l; i++) {
slice[i] = "";
}
return slice;
}
}

View File

@ -0,0 +1,85 @@
package org.ethereum.trie;
import java.util.List;
import org.ethereum.util.CompactEncoder;
import org.ethereum.util.Value;
public class TrieIterator {
Trie trie;
String key;
String value;
List<byte[]> shas;
List<String> values;
public TrieIterator(Trie t) {
this.trie = t;
}
// Some time in the near future this will need refactoring :-)
// XXX Note to self, IsSlice == inline node. Str == sha3 to node
private void workNode(Value currentNode) {
if (currentNode.length() == 2) {
byte[] k = CompactEncoder.decode(currentNode.get(0).asBytes());
if (currentNode.get(1).asString() == "") {
this.workNode(currentNode.get(1));
} else {
if (k[k.length-1] == 16) {
this.values.add( currentNode.get(1).asString() );
} else {
this.shas.add(currentNode.get(1).asBytes());
this.getNode(currentNode.get(1).asBytes());
}
}
} else {
for (int i = 0; i < currentNode.length(); i++) {
if (i == 16 && currentNode.get(i).length() != 0) {
this.values.add( currentNode.get(i).asString() );
} else {
if (currentNode.get(i).asString() == "") {
this.workNode(currentNode.get(i));
} else {
String val = currentNode.get(i).asString();
if (val != "") {
this.shas.add(currentNode.get(1).asBytes());
this.getNode(val.getBytes());
}
}
}
}
}
}
private void getNode(byte[] node) {
Value currentNode = this.trie.getCache().get(node);
this.workNode(currentNode);
}
private List<byte[]> collect() {
if (this.trie.getRoot() == "") {
return null;
}
this.getNode(new Value(this.trie.getRoot()).asBytes());
return this.shas;
}
public int purge() {
List<byte[]> shas = this.collect();
for (byte[] sha : shas) {
this.trie.getCache().delete(sha);
}
return this.values.size();
}
private String getKey() {
return "";
}
private String getValue() {
return "";
}
}

View File

@ -0,0 +1,108 @@
package org.ethereum.util;
import java.io.ByteArrayOutputStream;
import static java.util.Arrays.copyOfRange;
import static java.util.Arrays.copyOf;
import static org.ethereum.util.ByteUtil.appendByte;
import static org.spongycastle.util.Arrays.concatenate;
import static org.spongycastle.util.encoders.Hex.toHexString;
/**
*
* Compact encoding of hex sequence with optional terminator
*
* The traditional compact way of encoding a hex string is to convert it into binary
* - that is, a string like 0f1248 would become three bytes 15, 18, 72. However,
* this approach has one slight problem: what if the length of the hex string is odd?
* In that case, there is no way to distinguish between, say, 0f1248 and f1248.
*
* Additionally, our application in the Merkle Patricia tree requires the additional feature
* that a hex string can also have a special "terminator symbol" at the end (denoted by the 'T').
* A terminator symbol can occur only once, and only at the end.
*
* An alternative way of thinking about this to not think of there being a terminator symbol,
* but instead treat bit specifying the existence of the terminator symbol as a bit specifying
* that the given node encodes a final node, where the value is an actual value, rather than
* the hash of yet another node.
*
* To solve both of these issues, we force the first nibble of the final byte-stream to encode
* two flags, specifying oddness of length (ignoring the 'T' symbol) and terminator status;
* these are placed, respectively, into the two lowest significant bits of the first nibble.
* In the case of an even-length hex string, we must introduce a second nibble (of value zero)
* to ensure the hex-string is even in length and thus is representable by a whole number of bytes.
*
* Examples:
* > [ 1, 2, 3, 4, 5 ]
* '\x11\x23\x45'
* > [ 0, 1, 2, 3, 4, 5 ]
* '\x00\x01\x23\x45'
* > [ 0, 15, 1, 12, 11, 8, T ]
* '\x20\x0f\x1c\xb8'
* > [ 15, 1, 12, 11, 8, T ]
* '\x3f\x1c\xb8'
*
*/
public class CompactEncoder {
private final static byte TERMINATOR = 16;
private final static String hexBase = "0123456789abcdef";
public static byte[] encode(byte[] hexSlice) {
int terminator = 0;
if (hexSlice[hexSlice.length-1] == TERMINATOR) {
terminator = 1;
hexSlice = copyOf(hexSlice, hexSlice.length-1);
}
int oddlen = hexSlice.length % 2;
int flag = 2*terminator + oddlen;
if (oddlen != 0) {
byte[] flags = new byte[] { (byte) flag};
hexSlice = concatenate(flags, hexSlice);
} else {
byte[] flags = new byte[] { (byte) flag, 0};
hexSlice = concatenate(flags, hexSlice);
}
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
for (int i = 0; i < hexSlice.length; i += 2) {
buffer.write( 16*hexSlice[i] + hexSlice[i+1] );
}
return buffer.toByteArray();
}
/**
* Strips hex slices
*/
public static byte[] decode(byte[] str) {
byte[] base = hexDecode(str);
base = copyOf(base, base.length-1);
if (base[0] >= 2) {
base = appendByte(base, TERMINATOR);
}
if (base[0]%2 == 1) {
base = copyOfRange(base, 1, base.length);
} else {
base = copyOfRange(base, 2, base.length);
}
return base;
}
/**
* Transforms a binary array to hexadecimal format,
* returns array with each individual nibble adding a terminator at the end
*/
public static byte[] hexDecode(byte[] str) {
byte[] hexSlice = new byte[0];
String hexEncoded = toHexString(str);
for (char value : hexEncoded.toCharArray()) {
hexSlice = appendByte(hexSlice, (byte) hexBase.indexOf(value));
}
hexSlice = appendByte(hexSlice, TERMINATOR);
return hexSlice;
}
}

View File

@ -0,0 +1,19 @@
package org.ethereum.util;
public class DecodeResult {
private int pos;
private Object decoded;
public DecodeResult(int pos, Object decoded) {
this.pos = pos;
this.decoded = decoded;
}
public int getPos() {
return pos;
}
public Object getDecoded() {
return decoded;
}
}

View File

@ -0,0 +1,213 @@
package org.ethereum.util;
import java.math.BigInteger;
import java.util.ArrayList;
import static java.util.Arrays.copyOfRange;
import static org.spongycastle.util.Arrays.concatenate;
import java.util.List;
/**
* Recursive Length Prefix (RLP) encoding.
*
* The purpose of RLP is to encode arbitrarily nested arrays of binary data, and
* RLP is the main encoding method used to serialize objects in Ethereum. The
* only purpose of RLP is to encode structure; encoding specific atomic data
* types (eg. strings, ints, floats) is left up to higher-order protocols; in
* Ethereum the standard is that integers are represented in big endian binary
* form. If one wishes to use RLP to encode a dictionary, the two suggested
* canonical forms are to either use [[k1,v1],[k2,v2]...] with keys in
* lexicographic order or to use the higher-level Patricia Tree encoding as
* Ethereum does.
*
* The RLP encoding function takes in an item. An item is defined as follows:
*
* - A string (ie. byte array) is an item - A list of items is an item
*
* For example, an empty string is an item, as is the string containing the word
* "cat", a list containing any number of strings, as well as more complex data
* structures like ["cat",["puppy","cow"],"horse",[[]],"pig",[""],"sheep"]. Note
* that in the context of the rest of this article, "string" will be used as a
* synonym for "a certain number of bytes of binary data"; no special encodings
* are used and no knowledge about the content of the strings is implied.
*
* See: https://github.com/ethereum/wiki/wiki/%5BEnglish%5D-RLP
*/
public class RlpEncoder extends CompactEncoder {
/** Allow for content up to size of 2^64 bytes **/
private static double MAX_ITEM_LENGTH = Math.pow(256, 8);
/**
[5:30:35 PM] Vitalik Buterin: 56 bytes maximizes the benefit of both options
[5:30:41 PM] Vitalik Buterin: if we went with 60
[5:31:03 PM] Vitalik Buterin: then we would have only had 4 slots for long strings so RLP would not have been able to store objects above 4gb
[5:31:08 PM] Vitalik Buterin: if we went with 48
[5:31:18 PM] Vitalik Buterin: then RLP would be fine for 2^128 space, but that's way too much
[5:31:32 PM] Vitalik Buterin: so 56 and 2^64 space seems like the right place to put the cutoff
[5:31:44 PM] Vitalik Buterin: also, that's where Bitcoin's varint does the cutof
**/
private static int SIZE_THRESHOLD = 56;
/** RLP encoding rules are defined as follows: */
/*
* For a single byte whose value is in the [0x00, 0x7f] range, that byte is
* its own RLP encoding.
*/
/*
* If a string is 0-55 bytes long, the RLP encoding consists of a single
* byte with value 0x80 plus the length of the string followed by the
* string. The range of the first byte is thus [0x80, 0xb7].
*/
private static int offsetShortItem = 0x80;
/*
* If a string is more than 55 bytes long, the RLP encoding consists of a
* single byte with value 0xb7 plus the length of the length of the string
* in binary form, followed by the length of the string, followed by the
* string. For example, a length-1024 string would be encoded as
* \xb9\x04\x00 followed by the string. The range of the first byte is thus
* [0xb8, 0xbf].
*/
private static int offsetLongItem = 0xb8;
/*
* If the total payload of a list (i.e. the combined length of all its
* items) is 0-55 bytes long, the RLP encoding consists of a single byte
* with value 0xc0 plus the length of the list followed by the concatenation
* of the RLP encodings of the items. The range of the first byte is thus
* [0xc0, 0xf7].
*/
private static int offsetShortList = 0xc0;
/*
* If the total payload of a list is more than 55 bytes long, the RLP
* encoding consists of a single byte with value 0xf7 plus the length of the
* length of the list in binary form, followed by the length of the list,
* followed by the concatenation of the RLP encodings of the items. The
* range of the first byte is thus [0xf8, 0xff].
*/
private static int offsetLongList = 0xf8;
private static int maxPrefix = 0xff;
public static byte[] encode(Object input) {
Value val = new Value(input);
if (val.isList()) {
List<Object> inputArray = val.asList();
if (inputArray.size() == 0) {
return encodeLength(inputArray.size(), offsetShortList);
}
byte[] output = new byte[0];
for (Object object : inputArray) {
output = concatenate(output, encode(object));
}
byte[] prefix = encodeLength(output.length, offsetShortList);
return concatenate(prefix, output);
} else {
byte[] inputAsHex = asHex(input);
if(inputAsHex.length == 1) {
return inputAsHex;
} else {
byte[] firstByte = encodeLength(inputAsHex.length, offsetShortItem);
return concatenate(firstByte, inputAsHex);
}
}
}
public static DecodeResult decode(byte[] data, int pos) {
if (data == null || data.length < 1) {
return null;
}
int prefix = data[pos] & maxPrefix;
if (prefix == offsetShortItem) {
return new DecodeResult(pos+1, new byte[0]); // means no length or 0
} else if (prefix < offsetShortItem) {
return new DecodeResult(pos+1, new byte[] { data[pos] }); // byte is its own RLP encoding
} else if (prefix < offsetLongItem){
int len = prefix - offsetShortItem; // length of the encoded bytes
return new DecodeResult(pos+1+len, copyOfRange(data, pos+1, pos+1+len));
} else if (prefix < offsetShortList) {
int lenlen = prefix - offsetLongItem + 1; // length of length the encoded bytes
int lenbytes = toInt(copyOfRange(data, pos+1, pos+1+lenlen)); // length of encoded bytes
return new DecodeResult(pos+1+lenlen+lenbytes, copyOfRange(data, pos+1+lenlen, pos+1+lenlen+lenbytes));
} else if (prefix < offsetLongList) {
int len = prefix - offsetShortList; // length of the encoded list
int prevPos = pos; pos++;
return decodeList(data, pos, prevPos, len);
} else if (prefix < maxPrefix) {
int lenlen = prefix - offsetLongList + 1; // length of length the encoded list
int lenlist = toInt(copyOfRange(data, pos+1, pos+1+lenlen)); // length of encoded bytes
pos = pos + lenlen + 1;
int prevPos = lenlist;
return decodeList(data, pos, prevPos, lenlist);
} else {
throw new RuntimeException("Only byte values between 0x00 and 0xFF are supported, but got: " + prefix);
}
}
/** Integer limitation goes up to 2^31-1 so length can never be bigger than MAX_ITEM_LENGTH */
public static byte[] encodeLength(int length, int offset) {
if (length < SIZE_THRESHOLD) {
byte firstByte = (byte) (length + offset);
return new byte[] { firstByte };
} else if (length < MAX_ITEM_LENGTH) {
byte[] binaryLength = BigInteger.valueOf(length).toByteArray();
byte firstByte = (byte) (binaryLength.length + offset + SIZE_THRESHOLD - 1 );
return concatenate(new byte[] { firstByte }, binaryLength);
} else {
throw new RuntimeException("Input too long");
}
}
private static DecodeResult decodeList(byte[] data, int pos, int prevPos, int len) {
List<Object> slice = new ArrayList<Object>();
for (int i = 0; i < len;) {
// Get the next item in the data list and append it
DecodeResult result = decode(data, pos);
slice.add(result.getDecoded());
// Increment pos by the amount bytes in the previous read
prevPos = result.getPos();
i += (prevPos - pos);
pos = prevPos;
}
return new DecodeResult(pos, slice.toArray());
}
public static byte[] asHex(Object input) {
if (input instanceof byte[]) {
return (byte[]) input;
} else if (input instanceof String) {
String inputString = (String) input;
return inputString.getBytes();
} else if(input instanceof Integer) {
Integer inputInt = (Integer) input;
return (inputInt == 0) ? new byte[0] : BigInteger.valueOf(inputInt.longValue()).toByteArray();
} else if(input instanceof BigInteger) {
BigInteger inputBigInt = (BigInteger) input;
return (inputBigInt == BigInteger.ZERO) ? new byte[0] : inputBigInt.toByteArray();
} else if (input instanceof Value) {
Value val = (Value) input;
return asHex(val.asObj());
}
throw new RuntimeException("Unsupported type: Only accepting String, Integer and BigInteger for now");
}
/**
* Cast hex encoded value from byte[] to int
*
* Limited to Integer.MAX_VALUE: 2^32-1
*
* @param b array contains the hex values
* @return int value of all hex values together.
*/
public static int toInt(byte[] b) {
if (b == null || b.length == 0) {
return 0;
}
return new BigInteger(b).intValue();
}
}

View File

@ -0,0 +1,160 @@
package org.ethereum.util;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.List;
import com.cedarsoftware.util.DeepEquals;
/**
* Class to encapsulate an object and provide utilities for conversion
*/
public class Value {
private Object value;
public void fromRlpEncoded(byte[] data) {
if (data.length != 0) {
this.value = RlpEncoder.decode(data, 0).getDecoded();
}
}
public Value(Object obj) {
if (obj instanceof Value) {
this.value = ((Value) obj).asObj();
} else {
this.value = obj;
}
}
/* *****************
* Convert
* *****************/
public Object asObj() {
return value;
}
public List<Object> asList() {
Object[] valueArray = (Object[]) value;
return Arrays.asList(valueArray);
}
public int asInt() {
if (isInt()) {
return (Integer) value;
} else if (isBytes()) {
return new BigInteger(asBytes()).intValue();
}
return 0;
}
public long asLong() {
if (isLong()) {
return (Long) value;
} else if (isBytes()) {
return new BigInteger(asBytes()).longValue();
}
return 0;
}
public BigInteger asBigInt() {
return (BigInteger) value;
}
public String asString() {
if (isBytes()) {
return new String((byte[]) value);
} else if (isString()) {
return (String) value;
}
return "";
}
public byte[] asBytes() {
if(isBytes()) {
return (byte[]) value;
} else if(isString()) {
return asString().getBytes();
}
return new byte[0];
}
public int[] asSlice() {
return (int[]) value;
}
public Value get(int index) {
if(isList()) {
// Guard for OutOfBounds
if (asList().size() <= index) {
return new Value(null);
}
if (index < 0) {
throw new RuntimeException("Negative index not allowed");
}
return new Value(asList().get(index));
}
// If this wasn't a slice you probably shouldn't be using this function
return new Value(null);
}
/* *****************
* Utility
* *****************/
public byte[] encode() {
return RlpEncoder.encode(value);
}
public boolean cmp(Value o) {
return DeepEquals.deepEquals(this, o);
}
/* *****************
* Checks
* *****************/
public boolean isList() {
return value != null && value.getClass().isArray() && !value.getClass().getComponentType().isPrimitive();
}
public boolean isString() {
return value instanceof String;
}
public boolean isInt() {
return value instanceof Integer;
}
public boolean isLong() {
return value instanceof Long;
}
public boolean isBigInt() {
return value instanceof BigInteger;
}
public boolean isBytes() {
return value instanceof byte[];
}
public boolean isNull() {
return value == null;
}
public boolean isEmpty() {
return !isNull() && isList() && asList().size() == 0;
}
public int length() {
if (isList()) {
return asList().size();
} else if (isBytes()) {
return asBytes().length;
} else if (isString()) {
return asString().length();
}
return 0;
}
}

View File

@ -2,7 +2,7 @@ package org.ethereum.block;
import org.spongycastle.util.encoders.Hex;
import org.ethereum.crypto.HashUtil;
import org.ethereum.net.RLP;
import org.ethereum.net.rlp.RLP;
import org.ethereum.net.rlp.RLPList;
import org.ethereum.net.vo.Block;
import org.junit.Test;

View File

@ -12,6 +12,7 @@ import org.ethereum.net.message.HelloMessage;
import org.ethereum.net.message.NotInChainMessage;
import org.ethereum.net.message.PeersMessage;
import org.ethereum.net.message.TransactionsMessage;
import org.ethereum.net.rlp.RLP;
import org.ethereum.net.rlp.RLPList;
import org.ethereum.net.vo.Block;
import org.ethereum.net.vo.PeerData;

View File

@ -0,0 +1,132 @@
package org.ethereum.trie;
import java.io.IOException;
import org.iq80.leveldb.DB;
import org.iq80.leveldb.DBException;
import org.iq80.leveldb.DBIterator;
import org.iq80.leveldb.Range;
import org.iq80.leveldb.ReadOptions;
import org.iq80.leveldb.Snapshot;
import org.iq80.leveldb.WriteBatch;
import org.iq80.leveldb.WriteOptions;
public class MockDB implements DB {
private int addedItems;
@Override
public void close() throws IOException {
// TODO Auto-generated method stub
}
@Override
public void compactRange(byte[] arg0, byte[] arg1) throws DBException {
// TODO Auto-generated method stub
}
@Override
public WriteBatch createWriteBatch() {
// TODO Auto-generated method stub
return null;
}
@Override
public void delete(byte[] arg0) throws DBException {
// TODO Auto-generated method stub
}
@Override
public Snapshot delete(byte[] arg0, WriteOptions arg1)
throws DBException {
// TODO Auto-generated method stub
return null;
}
@Override
public byte[] get(byte[] arg0) throws DBException {
// TODO Auto-generated method stub
return null;
}
@Override
public byte[] get(byte[] arg0, ReadOptions arg1) throws DBException {
// TODO Auto-generated method stub
return null;
}
@Override
public long[] getApproximateSizes(Range... arg0) {
// TODO Auto-generated method stub
return null;
}
@Override
public String getProperty(String arg0) {
// TODO Auto-generated method stub
return null;
}
@Override
public Snapshot getSnapshot() {
// TODO Auto-generated method stub
return null;
}
@Override
public DBIterator iterator() {
// TODO Auto-generated method stub
return null;
}
@Override
public DBIterator iterator(ReadOptions arg0) {
// TODO Auto-generated method stub
return null;
}
@Override
public void put(byte[] arg0, byte[] arg1) throws DBException {
// TODO Auto-generated method stub
addedItems++;
}
@Override
public Snapshot put(byte[] arg0, byte[] arg1, WriteOptions arg2)
throws DBException {
// TODO Auto-generated method stub
return null;
}
@Override
public void resumeCompactions() {
// TODO Auto-generated method stub
}
@Override
public void suspendCompactions() throws InterruptedException {
// TODO Auto-generated method stub
}
@Override
public void write(WriteBatch arg0) throws DBException {
// TODO Auto-generated method stub
}
@Override
public Snapshot write(WriteBatch arg0, WriteOptions arg1)
throws DBException {
// TODO Auto-generated method stub
return null;
}
/**
* Returns the number of items added to this Mock DB
*
* @return int
*/
public int getAddedItems() {
return addedItems;
}
}

View File

@ -0,0 +1,305 @@
package org.ethereum.trie;
import static org.junit.Assert.*;
import org.ethereum.trie.Trie;
import org.junit.Test;
import com.cedarsoftware.util.DeepEquals;
public class TrieTest {
private static String LONG_STRING = "1234567890abcdefghijklmnopqrstuvwxxzABCEFGHIJKLMNOPQRSTUVWXYZ";
private static String c = "c";
private static String ca = "ca";
private static String cat = "cat";
private static String dog = "dog";
private static String doge = "doge";
private static String test = "test";
private static String dude = "dude";
private MockDB mockDb = new MockDB();
// ROOT: [ '\x16', A ]
// A: [ '', '', '', '', B, '', '', '', C, '', '', '', '', '', '', '', '' ]
// B: [ '\x00\x6f', D ]
// D: [ '', '', '', '', '', '', E, '', '', '', '', '', '', '', '', '', 'verb' ]
// E: [ '\x17', F ]
// F: [ '', '', '', '', '', '', G, '', '', '', '', '', '', '', '', '', 'puppy' ]
// G: [ '\x35', 'coin' ]
// C: [ '\x20\x6f\x72\x73\x65', 'stallion' ]
@Test
public void testEmptyKey() {
Trie trie = new Trie(mockDb);
trie.update("", dog);
String result = trie.get("");
assertEquals(dog, result);
}
@Test
public void testInsertShortString() {
Trie trie = new Trie(mockDb);
trie.update(cat, dog);
String result = trie.get(cat);
assertEquals(dog, result);
}
@Test
public void testInsertLongString() {
Trie trie = new Trie(mockDb);
trie.update(cat, LONG_STRING);
String result = trie.get(cat);
assertEquals(LONG_STRING, result);
}
@Test
public void testInsertMultipleItems1() {
Trie trie = new Trie(mockDb);
trie.update(ca, dude);
trie.update(cat, dog);
trie.update(dog, test);
trie.update(doge, LONG_STRING);
trie.update(test, LONG_STRING);
String result1 = trie.get(ca);
String result2 = trie.get(cat);
String result3 = trie.get(dog);
String result4 = trie.get(doge);
String result5 = trie.get(test);
assertEquals(dude, result1);
assertEquals(dog, result2);
assertEquals(test, result3);
assertEquals(LONG_STRING, result4);
assertEquals(LONG_STRING, result5);
}
@Test
public void testInsertMultipleItems2() {
Trie trie = new Trie(mockDb);
trie.update(cat, dog);
trie.update(ca, dude);
trie.update(doge, LONG_STRING);
trie.update(dog, test);
trie.update(test, LONG_STRING);
String result1 = trie.get(cat);
String result2 = trie.get(ca);
String result3 = trie.get(doge);
String result4 = trie.get(dog);
String result5 = trie.get(test);
assertEquals(dog, result1);
assertEquals(dude, result2);
assertEquals(LONG_STRING, result3);
assertEquals(test, result4);
assertEquals(LONG_STRING, result5);
}
@Test
public void testUpdateShortToShortString() {
Trie trie = new Trie(mockDb);
trie.update(cat, dog);
trie.update(cat, dog+"1");
String result = trie.get(cat);
assertEquals(dog+"1", result);
}
@Test
public void testUpdateLongToLongString() {
Trie trie = new Trie(mockDb);
trie.update(cat, LONG_STRING);
trie.update(cat, LONG_STRING+"1");
String result = trie.get(cat);
assertEquals(LONG_STRING+"1", result);
}
@Test
public void testUpdateShortToLongString() {
Trie trie = new Trie(mockDb);
trie.update(cat, dog);
trie.update(cat, LONG_STRING+"1");
String result = trie.get(cat);
assertEquals(LONG_STRING+"1", result);
}
@Test
public void testUpdateLongToShortString() {
Trie trie = new Trie(mockDb);
trie.update(cat, LONG_STRING);
trie.update(cat, dog+"1");
String result = trie.get(cat);
assertEquals(dog+"1", result);
}
@Test
public void testDeleteShortString1() {
Trie trie = new Trie(mockDb);
trie.update(cat, dog);
Object expected = trie.getRoot();
trie.update(ca, dude);
trie.delete(ca);
Object result = trie.getRoot();
assertTrue("Tries are not equal", DeepEquals.deepEquals(expected, result));
}
@Test
public void testDeleteShortString2() {
Trie trie = new Trie(mockDb);
trie.update(ca, dude);
Object expected = trie.getRoot();
trie.update(cat, dog);
trie.delete(cat);
Object result = trie.getRoot();
assertTrue("Tries are not equal", DeepEquals.deepEquals(expected, result));
}
@Test
public void testDeleteShortString3() {
Trie trie = new Trie(mockDb);
trie.update(cat, dude);
Object expected = trie.getRoot();
trie.update(dog, test);
trie.delete(dog);
Object result = trie.getRoot();
assertTrue("Tries are not equal", DeepEquals.deepEquals(expected, result));
}
@Test
public void testDeleteLongString1() {
Trie trie = new Trie(mockDb);
trie.update(cat, LONG_STRING);
Object expected = trie.getRoot();
trie.update(dog, LONG_STRING);
trie.delete(dog);
Object result = trie.getRoot();
assertTrue("Tries are not equal", DeepEquals.deepEquals(expected, result));
}
@Test
public void testDeleteLongString2() {
Trie trie = new Trie(mockDb);
trie.update(ca, LONG_STRING);
Object expected = trie.getRoot();
trie.update(cat, LONG_STRING);
trie.delete(cat);
Object result = trie.getRoot();
assertTrue("Tries are not equal", DeepEquals.deepEquals(expected, result));
}
@Test
public void testDeleteLongString3() {
Trie trie = new Trie(mockDb);
trie.update(cat, LONG_STRING);
Object expected = trie.getRoot();
trie.update(ca, LONG_STRING);
trie.delete(ca);
Object result = trie.getRoot();
assertTrue("Tries are not equal", DeepEquals.deepEquals(expected, result));
}
@Test
public void testDeleteMultipleItems1() {
Trie trie = new Trie(mockDb);
trie.update(cat, dog);
trie.update(ca, dude);
trie.update(doge, LONG_STRING);
Object expected = trie.getRoot();
trie.update(dog, test);
trie.update(test, LONG_STRING);
trie.delete(dog);
trie.delete(test);
Object result = trie.getRoot();
assertTrue("Tries are not equal", DeepEquals.deepEquals(expected, result));
}
@Test
public void testDeleteMultipleItems2() {
Trie trie = new Trie(mockDb);
trie.update(c, LONG_STRING);
Object expected = trie.getRoot();
trie.update(ca, LONG_STRING);
trie.update(cat, LONG_STRING);
trie.delete(ca);
trie.delete(cat);
Object result = trie.getRoot();
assertTrue("Tries are not equal", DeepEquals.deepEquals(expected, result));
}
@Test
public void testDeleteAll() {
Trie trie = new Trie(mockDb);
Object expected = trie.getRoot();
trie.update(ca, dude);
trie.update(cat, dog);
trie.update(doge, LONG_STRING);
trie.delete(ca);
trie.delete(cat);
trie.delete(doge);
Object result = trie.getRoot();
assertTrue("Tries are not equal", DeepEquals.deepEquals(expected, result));
}
@Test
public void testTrieCmp() {
Trie trie1 = new Trie(mockDb);
Trie trie2 = new Trie(mockDb);
trie1.update(doge, LONG_STRING);
trie2.update(doge, LONG_STRING);
assertTrue("Expected tries to be equal", trie1.cmp(trie2));
trie1.update(dog, LONG_STRING);
trie2.update(cat, LONG_STRING);
assertFalse("Expected tries not to be equal", trie1.cmp(trie2));
}
@Test
public void testTrieSync() {
Trie trie = new Trie(mockDb);
trie.update(dog, LONG_STRING);
assertEquals("Expected no data in database", mockDb.getAddedItems(), 0);
trie.sync();
assertNotEquals("Expected data to be persisted", mockDb.getAddedItems(), 0);
}
@Test
public void TestTrieDirtyTracking() {
Trie trie = new Trie(mockDb);
trie.update(dog, LONG_STRING);
assertTrue("Expected trie to be dirty", trie.getCache().isDirty());
trie.sync();
assertFalse("Expected trie not to be dirty", trie.getCache().isDirty());
trie.update(test, LONG_STRING);
trie.getCache().undo();
assertFalse("Expected trie not to be dirty", trie.getCache().isDirty());
}
@Test
public void TestTrieReset() {
Trie trie = new Trie(mockDb);
trie.update(cat, LONG_STRING);
assertNotEquals("Expected cached nodes", 0, trie.getCache().getNodes().size());
trie.getCache().undo();
assertEquals("Expected no nodes after undo", 0, trie.getCache().getNodes().size());
}
@Test
public void testTrieCopy() {
fail("To be implemented");
}
@Test
public void testTrieUndo() {
fail("To be implemented");
}
}

View File

@ -0,0 +1,74 @@
package org.ethereum.util;
import static org.junit.Assert.*;
import org.ethereum.util.CompactEncoder;
import org.junit.Test;
public class CompactEncoderTest {
private final static byte T = 16; // terminator
@Test
public void testCompactEncodeOddCompact() {
byte[] test = new byte[] { 1, 2, 3, 4, 5 };
byte[] expectedData = new byte[] { 0x11, 0x23, 0x45 };
assertArrayEquals("odd compact encode fail", expectedData, CompactEncoder.encode(test));
}
@Test
public void testCompactEncodeEvenCompact() {
byte[] test = new byte[] { 0, 1, 2, 3, 4, 5 };
byte[] expectedData = new byte[] { 0x00, 0x01, 0x23, 0x45 };
assertArrayEquals("even compact encode fail", expectedData, CompactEncoder.encode(test));
}
@Test
public void testCompactEncodeEvenTerminated() {
byte[] test = new byte[] { 0, 15, 1, 12, 11, 8, T };
byte[] expectedData = new byte[] { 0x20, 0x0f, 0x1c, (byte) 0xb8 };
assertArrayEquals("even terminated compact encode fail", expectedData, CompactEncoder.encode(test));
}
@Test
public void testCompactEncodeOddTerminated() {
byte[] test = new byte[] { 15, 1, 12, 11, 8, T };
byte[] expectedData = new byte[] { 0x3f, 0x1c, (byte) 0xb8 };
assertArrayEquals("odd terminated compact encode fail", expectedData, CompactEncoder.encode(test));
}
@Test
public void testCompactDecodeOddCompact() {
byte[] test = new byte[] { 0x11, 0x23, 0x45 };
byte[] expected = new byte[] {1, 2, 3, 4, 5};
assertArrayEquals("odd compact decode fail", expected, CompactEncoder.decode(test));
}
@Test
public void testCompactDecodeEvenCompact() {
byte[] test = new byte[] { 0x00, 0x01, 0x23, 0x45 };
byte[] expected = new byte[] {0, 1, 2, 3, 4, 5};
assertArrayEquals("even compact decode fail", expected, CompactEncoder.decode(test));
}
@Test
public void testCompactDecodeEvenTerminated() {
byte[] test = new byte[] { 0x20, 0x0f, 0x1c, (byte) 0xb8 };
byte[] expected = new byte[] {0, 15, 1, 12, 11, 8, T};
assertArrayEquals("even terminated compact decode fail", expected, CompactEncoder.decode(test));
}
@Test
public void testCompactDecodeOddTerminated() {
byte[] test = new byte[] { 0x3f, 0x1c, (byte) 0xb8 };
byte[] expected = new byte[] {15, 1, 12, 11, 8, T};
assertArrayEquals("odd terminated compact decode fail", expected, CompactEncoder.decode(test));
}
@Test
public void testCompactHexDecode() {
byte[] test = "stallion".getBytes();
byte[] result = new byte[] { 7, 3, 7, 4, 6, 1, 6, 12, 6, 12, 6, 9, 6, 15, 6, 14, T };
assertArrayEquals(result, CompactEncoder.hexDecode(test));
}
}

View File

@ -1,7 +1,8 @@
package org.ethereum.net;
package org.ethereum.util;
import org.spongycastle.util.encoders.Hex;
import org.ethereum.crypto.HashUtil;
import org.ethereum.net.rlp.RLP;
import org.ethereum.net.rlp.RLPList;
import org.ethereum.util.Utils;
import org.junit.Test;

View File

@ -0,0 +1,425 @@
package org.ethereum.util;
import static org.ethereum.util.RlpEncoder.toInt;
import static org.ethereum.util.RlpTestData.expected14;
import static org.ethereum.util.RlpTestData.expected16;
import static org.ethereum.util.RlpTestData.result01;
import static org.ethereum.util.RlpTestData.result02;
import static org.ethereum.util.RlpTestData.result03;
import static org.ethereum.util.RlpTestData.result04;
import static org.ethereum.util.RlpTestData.result05;
import static org.ethereum.util.RlpTestData.result06;
import static org.ethereum.util.RlpTestData.result07;
import static org.ethereum.util.RlpTestData.result08;
import static org.ethereum.util.RlpTestData.result09;
import static org.ethereum.util.RlpTestData.result10;
import static org.ethereum.util.RlpTestData.result11;
import static org.ethereum.util.RlpTestData.result12;
import static org.ethereum.util.RlpTestData.result13;
import static org.ethereum.util.RlpTestData.result14;
import static org.ethereum.util.RlpTestData.result15;
import static org.ethereum.util.RlpTestData.result16;
import static org.ethereum.util.RlpTestData.test01;
import static org.ethereum.util.RlpTestData.test02;
import static org.ethereum.util.RlpTestData.test03;
import static org.ethereum.util.RlpTestData.test04;
import static org.ethereum.util.RlpTestData.test05;
import static org.ethereum.util.RlpTestData.test06;
import static org.ethereum.util.RlpTestData.test07;
import static org.ethereum.util.RlpTestData.test08;
import static org.ethereum.util.RlpTestData.test09;
import static org.ethereum.util.RlpTestData.test10;
import static org.ethereum.util.RlpTestData.test11;
import static org.ethereum.util.RlpTestData.test12;
import static org.ethereum.util.RlpTestData.test13;
import static org.ethereum.util.RlpTestData.test14;
import static org.ethereum.util.RlpTestData.test15;
import static org.ethereum.util.RlpTestData.test16;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.math.BigInteger;
import java.util.Arrays;
import org.junit.Test;
import com.cedarsoftware.util.DeepEquals;
public class RlpEncoderTest {
/************************************
* Test data from: https://github.com/ethereum/wiki/wiki/%5BEnglish%5D-RLP
*
* Using assertEquals(String, String) instead of assertArrayEquals to see the actual content when the test fails.
*/
@Test(expected = RuntimeException.class)
public void testEncodeNull() {
RlpEncoder.encode(null);
}
@Test
public void testEncodeEmptyString() {
String test = "";
String expected = "80";
byte[] encoderesult = RlpEncoder.encode(test);
assertEquals(expected, asHex(encoderesult));
byte[] decodeResult = (byte[]) RlpEncoder.decode(encoderesult, 0).getDecoded();
assertEquals(test, bytesToAscii(decodeResult));
}
@Test
public void testEncodeShortString() {
String test = "dog";
String expected = "83646f67";
byte[] encoderesult = RlpEncoder.encode(test);
assertEquals(expected, asHex(encoderesult));
byte[] decodeResult = (byte[]) RlpEncoder.decode(encoderesult, 0).getDecoded();
assertEquals(test, bytesToAscii(decodeResult));
}
@Test
public void testEncodeSingleCharacter() {
String test = "d";
String expected = "64";
byte[] encoderesult = RlpEncoder.encode(test);
assertEquals(expected, asHex(encoderesult));
byte[] decodeResult = (byte[]) RlpEncoder.decode(encoderesult, 0).getDecoded();
assertEquals(test, bytesToAscii(decodeResult));
}
@Test
public void testEncodeLongString() {
String test = "Lorem ipsum dolor sit amet, consectetur adipisicing elit"; // length = 56
String expected = "b8384c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e7365637465747572206164697069736963696e6720656c6974";
byte[] encoderesult = RlpEncoder.encode(test);
assertEquals(expected, asHex(encoderesult));
byte[] decodeResult = (byte[]) RlpEncoder.decode(encoderesult, 0).getDecoded();
assertEquals(test, bytesToAscii(decodeResult));
}
@Test
public void testEncodeZero() {
Integer test = new Integer(0);
String expected = "80";
byte[] encoderesult = RlpEncoder.encode(test);
assertEquals(expected, asHex(encoderesult));
byte[] decodeResult = (byte[]) RlpEncoder.decode(encoderesult, 0).getDecoded();
int result = toInt(decodeResult);
assertEquals(test, Integer.valueOf(result));
}
@Test
public void testEncodeSmallInteger() {
Integer test = new Integer(15);
String expected = "0f";
byte[] encoderesult = RlpEncoder.encode(test);
assertEquals(expected, asHex(encoderesult));
byte[] decodeResult = (byte[]) RlpEncoder.decode(encoderesult, 0).getDecoded();
int result = toInt(decodeResult);
assertEquals(test, Integer.valueOf(result));
}
@Test
public void testEncodeMediumInteger() {
Integer test = new Integer(1000);
String expected = "8203e8";
byte[] encoderesult = RlpEncoder.encode(test);
assertEquals(expected, asHex(encoderesult));
byte[] decodeResult = (byte[]) RlpEncoder.decode(encoderesult, 0).getDecoded();
int result = toInt(decodeResult);
assertEquals(test, Integer.valueOf(result));
test = new Integer(1024);
expected = "820400";
encoderesult = RlpEncoder.encode(test);
assertEquals(expected, asHex(encoderesult));
decodeResult = (byte[]) RlpEncoder.decode(encoderesult, 0).getDecoded();
result = toInt(decodeResult);
assertEquals(test, Integer.valueOf(result));
}
@Test
public void testEncodeBigInteger() {
BigInteger test = new BigInteger("100102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", 16);
String expected = "a0100102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
byte[] encoderesult = RlpEncoder.encode(test);
assertEquals(expected, asHex(encoderesult));
byte[] decodeResult = (byte[]) RlpEncoder.decode(encoderesult, 0).getDecoded();
assertEquals(test, new BigInteger(decodeResult));
}
@Test
public void TestEncodeEmptyList() {
String[] test = new String[0];
String expected = "c0";
byte[] encoderesult = RlpEncoder.encode(test);
assertEquals(expected, asHex(encoderesult));
Object[] decodeResult = (Object[]) RlpEncoder.decode(encoderesult, 0).getDecoded();
assertTrue(decodeResult.length == 0);
}
@Test
public void testEncodeShortStringList() {
String[] test = new String[] { "cat", "dog" };
String expected = "c88363617483646f67";
byte[] encoderesult = RlpEncoder.encode(test);
assertEquals(expected, asHex(encoderesult));
Object[] decodeResult = (Object[]) RlpEncoder.decode(encoderesult, 0).getDecoded();
assertEquals("cat", bytesToAscii((byte[]) decodeResult[0]));
assertEquals("dog", bytesToAscii((byte[]) decodeResult[1]));
test = new String[] { "dog", "god", "cat" };
expected = "cc83646f6783676f6483636174";
encoderesult = RlpEncoder.encode(test);
assertEquals(expected, asHex(encoderesult));
decodeResult = (Object[]) RlpEncoder.decode(encoderesult, 0).getDecoded();
assertEquals("dog", bytesToAscii((byte[]) decodeResult[0]));
assertEquals("god", bytesToAscii((byte[]) decodeResult[1]));
assertEquals("cat", bytesToAscii((byte[]) decodeResult[2]));
}
@Test
public void testEncodeLongStringList() {
String element1 = "cat";
String element2 = "Lorem ipsum dolor sit amet, consectetur adipisicing elit";
String[] test = new String[] { element1, element2 };
String expected = "f83e83636174b8384c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e7365637465747572206164697069736963696e6720656c6974";
byte[] encoderesult = (byte[]) RlpEncoder.encode(test);
assertEquals(expected, asHex(encoderesult));
Object[] decodeResult = (Object[]) RlpEncoder.decode(encoderesult, 0).getDecoded();
assertEquals(element1, bytesToAscii((byte[]) decodeResult[0]));
assertEquals(element2, bytesToAscii((byte[]) decodeResult[1]));
}
//multilist:
//in: [ 1, ["cat"], "dog", [ 2 ] ],
//out: "cc01c48363617483646f67c102"
//in: [ [ ["cat"], ["dog"] ], [ [1] [2] ], [] ],
//out: "cdc88363617483646f67c20102c0"
@Test
public void testEncodeMultiList() {
Object[] test = new Object[] { 1, new Object[] { "cat" }, "dog", new Object[] { 2 } };
String expected = "cc01c48363617483646f67c102";
byte[] encoderesult = RlpEncoder.encode(test);
assertEquals(expected, asHex(encoderesult));
Object[] decodeResult = (Object[]) RlpEncoder.decode(encoderesult, 0).getDecoded();
assertEquals(1, toInt( (byte[]) decodeResult[0] ));
assertEquals("cat", bytesToAscii( ((byte[]) ((Object[]) decodeResult[1])[0] )));
assertEquals("dog", bytesToAscii( (byte[]) decodeResult[2]));
assertEquals(2, toInt( ((byte[]) ((Object[]) decodeResult[3])[0] )));
test = new Object[] { new Object[] { "cat", "dog" }, new Object[] { 1, 2 }, new Object[] { } };
expected = "cdc88363617483646f67c20102c0";
encoderesult = RlpEncoder.encode(test);
assertEquals(expected, asHex(encoderesult));
decodeResult = (Object[]) RlpEncoder.decode(encoderesult, 0).getDecoded();
assertEquals("cat", bytesToAscii( ((byte[]) ((Object[]) decodeResult[0])[0] )));
assertEquals("dog", bytesToAscii( ((byte[]) ((Object[]) decodeResult[0])[1] )));
assertEquals(1, toInt( ((byte[]) ((Object[]) decodeResult[1])[0] )));
assertEquals(2, toInt( ((byte[]) ((Object[]) decodeResult[1])[1] )));
assertTrue( ( ((Object[]) decodeResult[2]).length == 0 ));
}
@Test
public void testEncodeEmptyListOfList() {
// list = [ [ [], [] ], [] ],
Object[] test = new Object[] { new Object[] { new Object[] {}, new Object[] {} }, new Object[] {} };
String expected = "c4c2c0c0c0";
byte[] encoderesult = RlpEncoder.encode(test);
assertEquals(expected, asHex(encoderesult));
Object[] decodeResult = (Object[]) RlpEncoder.decode(encoderesult, 0).getDecoded();
assertTrue( decodeResult.length == 2 );
assertTrue( ( (Object[]) (decodeResult[0] ) ).length == 2);
assertTrue( ( (Object[]) (decodeResult[1] ) ).length == 0);
assertTrue( ( (Object[]) ( (Object[]) ( decodeResult[0] ) )[0]).length == 0);
assertTrue( ( (Object[]) ( (Object[]) ( decodeResult[0] ) )[1]).length == 0);
}
//The set theoretical representation of two
@Test
public void testEncodeRepOfTwoListOfList() {
//list: [ [], [[]], [ [], [[]] ] ]
Object[] test = new Object[] { new Object[] { }, new Object[] { new Object[] {} }, new Object[] { new Object[] {}, new Object[] { new Object[] { } } } };
String expected = "c7c0c1c0c3c0c1c0";
byte[] encoderesult = RlpEncoder.encode(test);
assertEquals(expected, asHex(encoderesult));
Object[] decodeResult = (Object[]) RlpEncoder.decode(encoderesult, 0).getDecoded();
assertTrue( decodeResult.length == 3 );
assertTrue( ( (Object[]) (decodeResult[0]) ).length == 0);
assertTrue( ( (Object[]) (decodeResult[1]) ).length == 1);
assertTrue( ( (Object[]) (decodeResult[2]) ).length == 2);
assertTrue( ( (Object[]) ( (Object[]) (decodeResult[1]) )[0]).length == 0);
assertTrue( ( (Object[]) ( (Object[]) (decodeResult[2]) )[0]).length == 0);
assertTrue( ( (Object[]) ( (Object[]) (decodeResult[2]) )[1]).length == 1);
assertTrue( ( (Object[]) ( (Object[]) ( (Object[]) (decodeResult[2]) )[1] )[0]).length == 0);
}
@Test
public void testRlpEncode() {
assertEquals(result01, asHex(RlpEncoder.encode(test01)));
assertEquals(result02, asHex(RlpEncoder.encode(test02)));
assertEquals(result03, asHex(RlpEncoder.encode(test03)));
assertEquals(result04, asHex(RlpEncoder.encode(test04)));
assertEquals(result05, asHex(RlpEncoder.encode(test05)));
assertEquals(result06, asHex(RlpEncoder.encode(test06)));
assertEquals(result07, asHex(RlpEncoder.encode(test07)));
assertEquals(result08, asHex(RlpEncoder.encode(test08)));
assertEquals(result09, asHex(RlpEncoder.encode(test09)));
assertEquals(result10, asHex(RlpEncoder.encode(test10)));
assertEquals(result11, asHex(RlpEncoder.encode(test11)));
assertEquals(result12, asHex(RlpEncoder.encode(test12)));
assertEquals(result13, asHex(RlpEncoder.encode(test13)));
assertEquals(result14, asHex(RlpEncoder.encode(test14)));
assertEquals(result15, asHex(RlpEncoder.encode(test15)));
assertEquals(result16, asHex(RlpEncoder.encode(test16)));
}
@Test
public void testRlpDecode() {
int pos = 0;
byte[] decodedByte;
byte[] decodedData;
Object[] decodedList;
decodedByte = (byte[]) RlpEncoder.decode(fromHex(result01), pos).getDecoded();
assertEquals(test01, toInt(decodedByte));
decodedData = (byte[]) RlpEncoder.decode(fromHex(result02), pos).getDecoded();
assertEquals(test02, bytesToAscii(decodedData));
decodedData = (byte[]) RlpEncoder.decode(fromHex(result03), pos).getDecoded();
assertEquals(test03, bytesToAscii(decodedData));
decodedData = (byte[]) RlpEncoder.decode(fromHex(result04), pos).getDecoded();
assertEquals(test04, bytesToAscii(decodedData));
decodedData = (byte[]) RlpEncoder.decode(fromHex(result05), pos).getDecoded();
assertEquals(test05, bytesToAscii(decodedData));
decodedList = (Object[]) RlpEncoder.decode(fromHex(result06), pos).getDecoded();
assertEquals(test06[0], bytesToAscii((byte[]) decodedList[0]));
assertEquals(test06[1], bytesToAscii((byte[]) decodedList[1]));
decodedList = (Object[]) RlpEncoder.decode(fromHex(result07), pos).getDecoded();
assertEquals(test07[0], bytesToAscii((byte[]) decodedList[0]));
assertEquals(test07[1], bytesToAscii((byte[]) decodedList[1]));
assertEquals(test07[2], bytesToAscii((byte[]) decodedList[2]));
// 1
decodedData = (byte[]) RlpEncoder.decode(fromHex(result08), pos).getDecoded();
assertEquals(test08, toInt(decodedData));
// 10
decodedData = (byte[]) RlpEncoder.decode(fromHex(result09), pos).getDecoded();
assertEquals(test09, toInt(decodedData));
// 100
decodedData = (byte[]) RlpEncoder.decode(fromHex(result10), pos).getDecoded();
assertEquals(test10, toInt(decodedData));
// 1000
decodedData = (byte[]) RlpEncoder.decode(fromHex(result11), pos).getDecoded();
assertEquals(test11, toInt(decodedData));
decodedData = (byte[]) RlpEncoder.decode(fromHex(result12), pos).getDecoded();
assertTrue(test12.compareTo(new BigInteger(decodedData)) == 0);
decodedData = (byte[]) RlpEncoder.decode(fromHex(result13), pos).getDecoded();
assertTrue(test13.compareTo(new BigInteger(decodedData)) == 0);
// Need to test with different expected value, because decoding doesn't recognize types
Object testObject1 = RlpEncoder.decode(fromHex(result14), pos).getDecoded();
assertTrue(DeepEquals.deepEquals(expected14, testObject1));
Object testObject2 = RlpEncoder.decode(fromHex(result15), pos).getDecoded();
assertTrue(DeepEquals.deepEquals(test15, testObject2));
// Need to test with different expected value, because decoding doesn't recognize types
Object testObject3 = RlpEncoder.decode(fromHex(result16), pos).getDecoded();
assertTrue(DeepEquals.deepEquals(expected16, testObject3));
}
@Test
public void testEncodeLength() {
int length;
int offset;
byte[] encodedLength;
String expected;
// length < 56
length = 1; offset = 128;
encodedLength = RlpEncoder.encodeLength(length, offset);
expected = "81";
assertEquals(expected, asHex(encodedLength));
// 56 > length < 2^64
length = 56; offset = 192;
encodedLength = RlpEncoder.encodeLength(length, offset);
expected = "f838";
assertEquals(expected, asHex(encodedLength));
// length > 2^64
// TODO: Fix this test - when casting double to int, information gets lost since 'int' is max (2^31)-1
double maxLength = Math.pow(256, 8); offset = 192;
try {
encodedLength = RlpEncoder.encodeLength( (int) maxLength, offset);
System.out.println("length: " + length + ", offset: " + offset + ", encoded: " + Arrays.toString(encodedLength));
fail("Expecting RuntimeException: 'Input too long'");
} catch(RuntimeException e) {
// Success!
}
}
// Code from: http://stackoverflow.com/a/9855338/459349
protected final static char[] hexArray = "0123456789abcdef".toCharArray();
private static String asHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for ( int j = 0; j < bytes.length; j++ ) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
// Code from: http://stackoverflow.com/a/4785776/459349
private String bytesToAscii(byte[] b) {
String hex = asHex(b);
StringBuilder output = new StringBuilder();
for (int i = 0; i < hex.length(); i+=2) {
String str = hex.substring(i, i+2);
output.append((char)Integer.parseInt(str, 16));
}
return output.toString();
}
// Code from: http://stackoverflow.com/a/140861/459349
public static byte[] fromHex(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i+1), 16));
}
return data;
}
}

View File

@ -0,0 +1,59 @@
package org.ethereum.util;
import java.math.BigInteger;
public class RlpTestData {
/***********************************
* https://github.com/ethereum/tests/blob/master/rlptest.txt
*/
public static int test01 = 0;
public static String result01 = "80";
public static String test02 = "";
public static String result02 = "80";
public static String test03 = "d";
public static String result03 = "64";
public static String test04 = "cat";
public static String result04 = "83636174";
public static String test05 = "dog";
public static String result05 = "83646f67";
public static String[] test06 = new String[] { "cat", "dog" };
public static String result06 = "c88363617483646f67";
public static String[] test07 = new String[] { "dog", "god", "cat" };
public static String result07 = "cc83646f6783676f6483636174";
public static int test08 = 1;
public static String result08 = "01";
public static int test09 = 10;
public static String result09 = "0a";
public static int test10 = 100;
public static String result10 = "64";
public static int test11 = 1000;
public static String result11 = "8203e8";
public static BigInteger test12 = new BigInteger("115792089237316195423570985008687907853269984665640564039457584007913129639935");
public static String result12 = "a100ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
public static BigInteger test13 = new BigInteger("115792089237316195423570985008687907853269984665640564039457584007913129639936");
public static String result13 = "a1010000000000000000000000000000000000000000000000000000000000000000";
public static Object[] test14 = new Object[] { 1, 2, new Object[] {} };
public static String result14 = "c30102c0";
public static Object[] expected14 = new Object[] { new byte[] { 1 }, new byte[] { 2 }, new Object[] {} };
public static Object[] test15 = new Object[] { new Object[] { new Object[] {}, new Object[] {} }, new Object[] {} };
public static String result15 = "c4c2c0c0c0";
public static Object[] test16 = new Object[] { "zw", new Object[] { 4 }, "wz" };
public static String result16 = "c8827a77c10482777a";
public static Object[] expected16 = new Object[] { new byte[] { 122, 119 }, new Object[] { new byte[] { 4 } }, new byte[] { 119, 122 } };
}

View File

@ -0,0 +1,47 @@
package org.ethereum.util;
import static org.junit.Assert.*;
import java.math.BigInteger;
import java.util.Arrays;
import org.junit.Test;
public class ValueTest {
@Test
public void testCmp() {
Value val1 = new Value("hello");
Value val2 = new Value("world");
assertFalse("Expected values not to be equal", val1.cmp(val2));
Value val3 = new Value("hello");
Value val4 = new Value("hello");
assertTrue("Expected values to be equal", val3.cmp(val4));
}
@Test
public void testTypes() {
Value str = new Value("str");
assertEquals(str.asString(), "str");
Value num = new Value(1);
assertEquals(num.asInt(), 1);
Value inter = new Value(new Object[]{1});
Object[] interExp = new Object[]{1};
assertTrue(new Value(inter.asObj()).cmp(new Value(interExp)));
Value byt = new Value(new byte[]{1, 2, 3, 4});
byte[] bytExp = new byte[]{1, 2, 3, 4};
assertTrue(Arrays.equals(byt.asBytes(), bytExp));
Value bigInt = new Value(BigInteger.valueOf(10));
BigInteger bigExp = BigInteger.valueOf(10);
assertEquals(bigInt.asBigInt(), bigExp);
}
}